informasi kontak saya
Surat[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Apakah objek di Java dialokasikan di memori heap?
Oke, ini terlalu abstrak. Mari kita lebih spesifik. Mari kita lihat di mana memori objek berikut dialokasikan?
- public void test() {
- Object object = new Object();
- }
- 这个方法中的object对象,是在堆中分配内存么?
Katakan hasilnya: objek dapat mengalokasikan memori pada stack atau heap.
Inilah poin kuncinya: Dalam implementasi JVM, untuk meningkatkan kinerja JVM dan menghemat ruang memori, JVM menyediakan fitur yang disebut "analisis escape". Analisis escape adalah teknologi optimasi yang relatif mutakhir saat ini Mesin virtual Java, dan juga merupakan JIT Teknik optimasi yang sangat penting. jdk6 baru mulai memperkenalkan teknologi ini, jdk7 mulai mengaktifkan analisis escape secara default, jdk8 mulai meningkatkan analisis escape, dan mengaktifkannya secara default. Hingga JDK 9, analisis escape akan digunakan sebagai metode pengoptimalan default, dan tidak ada parameter kompilasi khusus diperlukan.
Sekarang pahami kalimat "objek dapat mengalokasikan memori di tumpukan atau mengalokasikan memori di tumpukan". Sebelum jdk7, objek di sini harus mengalokasikan memori di tumpukan di jdk7 dan 8, dimungkinkan untuk mengalokasikan memori di tumpukan, karena jdk7 hanya analisis Escape yang mulai didukung; jdk9 kemungkinan besar dialokasikan di tumpukan (objek di sini sangat kecil), karena jdk9 hanya benar-benar mendukung dan mengaktifkan analisis escape secara default.
Dengan perkembangan kompiler JIT (kompiler just-in-time) dan kematangan teknologi analisis escape secara bertahap, alokasi tumpukan dan teknologi optimasi penggantian skalar akan menyebabkan "semua objek akan dialokasikan di heap" menjadi kurang absolut di Java In Pada mesin virtual, objek dialokasikan memorinya di heap, namun ada kasus khusus, yaitu jika setelah analisis escape ditemukan bahwa suatu objek tidak lolos dari metode, maka dapat dioptimalkan untuk dialokasikan di stack. Ketika metode ini dijalankan Setelah selesai, bingkai tumpukan akan muncul dan objek dilepaskan. Hal ini menghilangkan kebutuhan untuk mengalokasikan memori pada tumpukan dan menjalani pengumpulan sampah (Hotspot saat ini tidak melakukan hal ini). Ini juga merupakan teknologi penyimpanan off-heap yang paling umum.
Setelah JDK 6u23 (versi utama yang dapat diingat JDK7), analisis escape diaktifkan secara default di Hotspot. Jika Anda menggunakan versi sebelumnya, Anda dapat menampilkan analisis escape melalui opsi "-XX:+DoEscapeAnalysis". Lihat hasil filter untuk analisis pelepasan.
Hotspot mengimplementasikan penggantian skalar melalui analisis escape (objek yang tidak di-escape diganti dengan skalar dan agregat, yang dapat meningkatkan efisiensi kode), tetapi objek yang tidak di-escape akan tetap mengalokasikan memori di heap, sehingga masih dapat dikatakan bahwa semua objek adalah Alokasikan memori di tumpukan.
Selain itu, sangat disesuaikan berdasarkan Open JDKMesin Virtual TaoBao, di antaranya teknologi inovatif GCIH (GC tak terlihat heap) menerapkan off-heap, memindahkan objek dengan siklus hidup yang panjang dari heap ke luar heap, dan GC tidak mengelola objek Java di dalam GCIH, sehingga mengurangi frekuensi daur ulang GC dan meningkatkan tujuannya efisiensi pemulihan GC.
Tumpukan: Ketika setiap metode dijalankan, bingkai tumpukan akan dibuat pada saat yang sama untuk menyimpan informasi seperti tabel variabel lokal, tumpukan operasi, koneksi dinamis, keluar metode, dll. Proses dari setiap metode dipanggil hingga eksekusi selesai sesuai dengan proses bingkai tumpukan dari didorong ke dalam tumpukan hingga dikeluarkan dari tumpukan di tumpukan mesin virtual.
tumpukan:Ketika suatu objek dipakai, objek tersebut dialokasikan pada heap dan referensi ke heap dimasukkan ke dalam tumpukan.
melarikan diri:Ketika sebuah pointer ke suatu objek direferensikan oleh beberapa metode atau thread, kita mengatakan bahwa pointer tersebut lolos. Umumnya, objek yang dikembalikan dan variabel global umumnya lolos.
Analisis pelarian:Metode yang digunakan untuk menganalisis fenomena pelarian ini disebut analisis pelarian
Pengoptimalan analisis escape - alokasi pada tumpukan:Alokasi di tumpukan berarti bahwa instance yang dihasilkan oleh variabel lokal dalam metode (tidak terjadi escape) dialokasikan di tumpukan dan tidak perlu dialokasikan di tumpukan. Setelah alokasi selesai, eksekusi dilanjutkan di tumpukan panggilan. Terakhir, thread berakhir, ruang tumpukan didaur ulang, dan objek Variabel lokal juga didaur ulang.
Jawaban: Belum tentu.
Jika kondisi analisis escape terpenuhi, sebuah objek dapat dialokasikan pada stack.Kurangi alokasi memori heap dan tekanan GC.Karena memori tumpukan terbatas, jika objek memenuhi kondisi penggantian skalar,Operasi lebih lanjut dilakukan pada subjek untuk memecahnya menjadi beberapa bagian.Metode spesifik penggantian skalar adalah: JVM selanjutnya akan memecah objek dan menguraikan objek menjadi beberapa variabel anggota yang digunakan oleh metode ini.Dengan demikian, tujuan pemanfaatan memori tumpukan dan register yang lebih baik tercapai.
Ini adalah algoritma analisis aliran data global lintas fungsi yang secara efektif dapat mengurangi beban sinkronisasi dan tekanan alokasi tumpukan memori dalam program Java. Melalui analisis escape, kompiler Java Hotspot dapat menganalisis rentang penggunaan referensi objek baru dan memutuskan apakah akan mengalokasikan objek tersebut ke heap.
Perilaku dasar analisis pelarian adalah menganalisis ruang lingkup dinamis objek:
Dalam prinsip optimasi kompiler bahasa komputer, analisis escape mengacu pada metode analisis rentang dinamis pointer. Hal ini terkait dengan analisis pointer dan analisis bentuk dari prinsip optimasi kompiler. Ketika suatu variabel (atau objek) dialokasikan dalam suatu metode, penunjuknya dapat dikembalikan atau direferensikan secara global, yang akan direferensikan oleh metode atau utas lain. Fenomena ini disebut pelarian penunjuk (atau referensi). Dalam istilah awam, jika penunjuk suatu objek direferensikan oleh beberapa metode atau utas, maka kita memanggil penunjuk objek (atau objek) Escape (karena saat ini, objek tersebut lolos dari cakupan lokal metode atau utas).
Deskripsi singkat: "Analisis escape: Analisis statis yang menentukan rentang dinamis pointer. Analisis ini dapat menganalisis di mana pointer dapat diakses dalam program." Dalam konteks kompilasi just-in-time JVM, analisis escape akan menentukan apakah objek yang baru dibuat lolos.
Dasar kompilasi just-in-time untuk menentukan apakah suatu objek lolos: yang pertama adalah apakah objek tersebut disimpan di heap (bidang statis atau bidang instance objek di heap), dan yang lainnya adalah apakah objek tersebut diteruskan ke kode yang tidak diketahui.
Analisis Escape saat ini merupakan teknologi pengoptimalan yang relatif mutakhir di mesin virtual Java. Seperti analisis hubungan pewarisan tipe, ini bukan sarana untuk mengoptimalkan kode secara langsung, namun merupakan teknologi analisis yang memberikan dasar untuk cara pengoptimalan lainnya.
Analisis Escape: Ini adalah teknologi optimasi JIT yang sangat penting, digunakan untuk menentukan apakah suatu objek akan diakses di luar metode, yaitu keluar dari cakupan metode. Analisis escape adalah langkah kompiler JIT. Melalui JIT kita dapat menentukan objek mana yang dapat dibatasi untuk digunakan di dalam metode dan tidak akan lolos ke luar. Kemudian objek tersebut dapat dioptimalkan, seperti mengalokasikannya ke dalam tumpukan, bukan di heap . Atau lakukan penggantian skalar untuk membagi objek menjadi beberapa tipe dasar untuk penyimpanan. Ini adalah algoritma analisis aliran data global lintas fungsi yang secara efektif dapat mengurangi beban sinkronisasi dan alokasi tumpukan memori serta tekanan pengumpulan sampah dalam program Java. Melalui analisis escape, kompiler Java Hotspot dapat menganalisis rentang penggunaan referensi objek baru dan memutuskan apakah akan mengalokasikan objek ini ke heap.
Analisis escape terutama berfokus pada variabel lokal untuk menentukan apakah objek yang dialokasikan di heap telah lolos dari cakupan metode. Hal ini terkait dengan analisis pointer dan analisis bentuk prinsip optimasi kompiler. Ketika suatu variabel (atau objek) dialokasikan dalam suatu metode, penunjuknya dapat dikembalikan atau direferensikan secara global, yang akan direferensikan oleh metode atau utas lain. Fenomena ini disebut pelarian penunjuk (atau referensi). Dalam istilah awam, jika penunjuk suatu objek direferensikan oleh beberapa metode atau thread, maka kita katakan bahwa penunjuk objek tersebut telah lolos. Merancang struktur kode dan penggunaan data dengan benar dapat memanfaatkan analisis escape dengan lebih baik untuk mengoptimalkan kinerja program. Kita juga dapat mengurangi overhead pengalokasian objek di heap dan meningkatkan pemanfaatan memori melalui analisis escape.
Analisis escape adalah teknik yang digunakan untuk menentukan apakah suatu objek lolos di luar cakupan suatu metode selama masa pakainya. Dalam pengembangan Java, analisis escape digunakan untuk menentukan siklus hidup dan cakupan objek untuk melakukan optimasi yang sesuai dan meningkatkan kinerja program dan efisiensi pemanfaatan memori.
Saat sebuah objek dibuat, objek tersebut dapat digunakan di dalam suatu metode, atau dapat diteruskan ke metode atau thread lain dan terus ada di luar metode tersebut. Jika objek tidak lolos dari cakupan metode, JVM dapat mengalokasikannya ke tumpukan, bukan di heap, sehingga menghindari overhead alokasi memori heap dan pengumpulan sampah.
- 关于逃逸分析的论文在1999年就已经发表,但直到Sun JDK 1.6才实现了逃逸分析,而且直到现在这项优化尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。如果要完全准确的判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。这是一个相对高耗时的过程,如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。
- 在测试结果中,实施逃逸分析后的程序在MicroBenchmarks中往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即使编译的收益)有所下降,所以在很长的一段时间里,即使是Server Compiler,也默认不开启逃逸分析(在JDK 1.6 Update 23的Server Compiler中才开始默认开启了逃逸分析),甚至在某些版本(如JDK 1.6 Update 18)中还曾经短暂的完全禁止了这项优化。
- 如果有需要,并且确认对程序运行有益,用户可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析,开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可以使用参数-XX:EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消除,使用参数-XX:PrintEliminateAllocations查看标量的替换情况。
- 尽管目前逃逸分析的技术仍不是十分成熟,但是他却是即时编译器优化技术的一个重要的方向,在今后的虚拟机中,逃逸分析技术肯定会支撑起一系列使用有效的优化技术。
Prinsip dasar analisis escape JVM adalah menentukan situasi escape suatu objek melalui dua metode analisis: statis dan dinamis.
Dalam sistem kompilasi Java, proses mengubah file kode sumber Java menjadi instruksi mesin yang dapat dijalankan komputer memerlukan dua tahap kompilasi:
Bagian pertama kompilasi mengacu pada kompiler front-endfile .javadikonversi kefile .kelas (file kode byte). Produk kompiler front-end dapat berupa Javac JDK atau kompiler tambahan di Eclipse JDT.
Pada tahap kompilasi kedua, JVM menafsirkan bytecode dan menerjemahkannya ke dalam instruksi mesin yang sesuai, membaca bytecode satu per satu, dan menafsirkan serta menerjemahkannya ke dalam kode mesin satu per satu.
Jelasnya, karena proses interpretasi perantara, kecepatan eksekusinya pasti akan jauh lebih lambat dibandingkan dengan program bytecode biner yang dapat dieksekusi. Ini adalah fungsi juru bahasa JVM tradisional (Interpreter).
Untuk mengatasi masalah efisiensi ini, diperkenalkan teknologi JIT (Just In Time Compiler).
Setelah diperkenalkannya teknologi JIT, program Java masih diinterpretasikan dan dieksekusi melalui interpreter. Artinya, bagian utama masih diinterpretasikan dan dieksekusi, namun tautan perantara dihilangkan sebagian.
Kompiler JIT (Just-in-timeCompiler) kompilasi tepat waktu. Solusi implementasi Java yang paling awal terdiri dari sekumpulan penerjemah (interpreter) yang menerjemahkan setiap instruksi Java ke dalam instruksi mikroprosesor yang setara, dan mengeksekusinya secara berurutan sesuai dengan urutan instruksi yang diterjemahkan, karena instruksi Java dapat Diterjemahkan ke dalam selusin atau lusinan instruksi instruksi mikroprosesor yang setara, mode ini dijalankan dengan sangat lambat.
Ketika JVM menemukan bahwa metode atau blok kode tertentu sering dijalankan, JVM akan menganggapnya sebagai "Kode Hot Spot". Kemudian JIT akan menerjemahkan bagian dari "kode panas" ke dalam kode mesin yang terkait dengan mesin lokal, mengoptimalkannya, dan kemudian menyimpan kode mesin yang diterjemahkan ke dalam cache untuk penggunaan berikutnya.
Di mana menyimpan kode mesin yang diterjemahkan ke dalam cache? Cache ini disebut Cache Kode. Terlihat bahwa metode untuk mencapai konkurensi tinggi antara aplikasi JVM dan WEB serupa, dan masih menggunakan arsitektur cache.
Ketika JVM menemukan kode panas yang sama di lain waktu, ia melewatkan tautan interpretasi perantara, memuat kode mesin langsung dari Code Cache, dan mengeksekusinya secara langsung tanpa mengkompilasi lagi.
Oleh karena itu, tujuan keseluruhan JIT adalah untuk menemukan kode panas, dan kode panas telah menjadi kunci untuk meningkatkan kinerja. Inilah asal mula nama hotspot JVM. Ini adalah upaya seumur hidup untuk mengidentifikasi kode panas dan menuliskannya di nama.
Oleh karena itu, strategi keseluruhan JVM adalah:
Untuk sebagian besar kode yang tidak umum, kita tidak perlu menghabiskan waktu untuk mengkompilasinya menjadi kode mesin, tetapi menjalankannya melalui interpretasi dan eksekusi;
Sedangkan untuk hot code yang hanya menempati sebagian kecil saja, kita dapat mengkompilasinya menjadi kode mesin untuk mencapai kecepatan lari yang ideal.
Munculnya JIT (kompilasi just in time) dan perbedaan antar interpreter
(1) Penerjemah menafsirkan bytecode ke dalam kode mesin. Bahkan jika ia menemukan bytecode yang sama di lain waktu, ia akan tetap melakukan interpretasi berulang.
(2) JIT mengkompilasi beberapa bytecode menjadi kode mesin dan menyimpannya di Code Cache. Ketika menemukan kode yang sama di lain waktu, kode tersebut akan dieksekusi secara langsung tanpa dikompilasi lagi.
(3) Penerjemah menafsirkan bytecode menjadi kode mesin yang umum untuk semua platform.
(4) JIT akan menghasilkan kode mesin khusus platform berdasarkan jenis platform.
JVM berisi beberapa kompiler just-in-time, terutama C1 dan C2, dan Graal (eksperimental).
Beberapa kompiler just-in-time akan mengoptimalkan bytecode dan menghasilkan kode mesin
JVM membagi status eksekusi menjadi 5 level:
Tingkat 0, Penerjemah
Level 1, dikompilasi dan dieksekusi menggunakan kompiler just-in-time C1 (tanpa pembuatan profil)
Lapisan 2, dikompilasi dan dieksekusi menggunakan kompiler just-in-time C1 (dengan pembuatan profil dasar)
Lapisan 3, dikompilasi dan dieksekusi menggunakan kompiler just-in-time C1 (dengan pembuatan profil lengkap)
Level 4, dikompilasi dan dieksekusi menggunakan kompiler just-in-time C2
JVM tidak akan mengaktifkan C2 secara langsung, melainkan mengumpulkan status program yang berjalan terlebih dahulu melalui kompilasi C1, lalu menentukan apakah akan mengaktifkan C2 berdasarkan hasil analisis.
Dalam mode kompilasi berlapis, status eksekusi mesin virtual dibagi menjadi lima lapisan dari yang sederhana hingga yang kompleks, dari cepat hingga lambat.
Selama kompilasi, selain melakukan cache kode panas untuk mempercepat proses, JIT juga akan melakukan banyak optimasi pada kode tersebut.
Tujuan dari beberapa optimasi adalah untukKurangi tekanan alokasi tumpukan memori , salah satu teknik penting dalam optimasi JIT disebut analisis escape. Menurut analisis escape, kompiler just-in-time akan mengoptimalkan kode sebagai berikut selama proses kompilasi:
Ia memeriksa struktur statis kode untuk menentukan apakah objek dapat lolos. Misalnya, ketika sebuah objek ditugaskan ke variabel anggota kelas atau dikembalikan ke metode eksternal, dapat ditentukan bahwa objek tersebut lolos.
Ini menentukan apakah suatu objek lolos dengan mengamati perilaku pemanggilan metode dan referensi objek. Misalnya, ketika suatu objek direferensikan oleh beberapa thread, objek tersebut dapat dinilai telah lolos.
Analisis escape melakukan analisis mendalam terhadap kode untuk menentukan apakah objek lolos di luar cakupan metode selama masa pakai metode. Jika objek tidak lolos, JVM dapat mengalokasikannya ke stack, bukan ke heap.
Sebuah objek memiliki tiga keadaan escape: escape global, parameter escape, dan no escape.
pelarian global(GlobalEscape): Artinya, cakupan suatu objek lolos dari metode saat ini atau thread saat ini.
Umumnya ada skenario berikut:
① Objeknya adalah variabel statis
② Objek adalah objek yang lolos
③ Objek digunakan sebagai nilai kembalian dari metode saat ini
Pelarian parameter(ArgEscape): Artinya, suatu objek dilewatkan sebagai parameter metode atau direferensikan oleh parameter, tetapi tidak ada escape global yang terjadi selama proses pemanggilan.
tidak ada jalan keluar: Artinya, objek dalam metode tidak lolos.
Kode contoh status escape adalah sebagai berikut:
- public class EscapeAnalysisTest {
-
- public static Object globalVariableObject;
-
- public Object instanceObject;
-
- public void globalVariableEscape(){
- globalVariableObject = new Object(); // 静态变量,外部线程可见,发生逃逸
- }
-
- public void instanceObjectEscape(){
- instanceObject = new Object(); // 赋值给堆中实例字段,外部线程可见,发生逃逸
- }
-
- public Object returnObjectEscape(){
- return new Object(); // 返回实例,外部线程可见,发生逃逸
- }
-
- public void noEscape(){
- Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸
- }
-
- }
1. Pelarian metode: Dalam isi metode, tentukan variabel lokal, yang dapat direferensikan oleh metode eksternal, seperti diteruskan ke metode sebagai parameter pemanggil, atau dikembalikan secara langsung sebagai objek. Atau, dapat dipahami bahwa objek tersebut melompat keluar dari metode.
Metode lolos meliputi:
- 我们可以用下面的代码来表示这个现象。
-
- //StringBuffer对象发生了方法逃逸
- public static StringBuffer createStringBuffer(String s1, String s2) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- return sb;
- }
- 上面的例子中,StringBuffer 对象通过return语句返回。
-
- StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
-
- 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
-
- 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
-
- 具体的代码如下:
-
- // 非方法逃逸
- public static String createString(String s1, String s2) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- return sb.toString();
- }
- 可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。
Bagaimana cara cepat menentukan apakah analisis escape telah terjadi? Mari kita lihat apakah entitas objek baru dipanggil di luar metode.
- public class EscapeAnalysis {
-
- public EscapeAnalysis obj;
-
- /**
- * 方法返回EscapeAnalysis对象,发生逃逸
- * @return
- */
- public EscapeAnalysis getInstance() {
- return obj == null ? new EscapeAnalysis():obj;
- }
-
- /**
- * 为成员属性赋值,发生逃逸
- */
- public void setObj() {
- this.obj = new EscapeAnalysis();
- }
-
- /**
- * 对象的作用于仅在当前方法中有效,没有发生逃逸
- */
- public void useEscapeAnalysis() {
- EscapeAnalysis e = new EscapeAnalysis();
- }
-
- /**
- * 引用成员变量的值,发生逃逸
- */
- public void useEscapeAnalysis2() {
- EscapeAnalysis e = getInstance();
- }
- }
2. Thread escape: Objek ini diakses oleh thread lain, seperti ditugaskan ke variabel instan dan diakses oleh thread lain. Objek lolos dari thread saat ini.
Analisis escape dapat membawa strategi pengoptimalan berikut ke program Java: alokasi pada tumpukan, penghapusan sinkronisasi, penggantian skalar, dan penyebarisan metode;
Parameter terkait analisis pelarian:
- -XX:+DoEscapeAnalysis 开启逃逸分析
- -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
- -XX:+EliminateAllocations 开启标量替换
- -XX:+EliminateLocks 开启同步消除
- -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。
Analisis escape dapat menentukan objek mana yang tidak akan lolos dari cakupan metode dan mengalokasikan objek tersebut di tumpukan, bukan di heap. Objek yang dialokasikan pada tumpukan dibuat dan dimusnahkan dalam siklus hidup pemanggilan metode tanpa pengumpulan sampah, sehingga meningkatkan efisiensi eksekusi program.
Dalam keadaan normal, objek yang tidak dapat keluar menempati ruang yang relatif besar. Jika ruang pada tumpukan dapat digunakan, sejumlah besar objek akan hancur saat metode berakhir, sehingga mengurangi tekanan GC.
Alokasi ide pada tumpukanAlokasi pada tumpukan adalah teknologi optimasi yang disediakan oleh JVM.
Idenya adalah:
Masalah: Karena memori tumpukan relatif kecil, objek besar tidak dapat dan tidak cocok untuk dialokasikan pada tumpukan.
Aktifkan alokasi on-stack
Alokasi pada tumpukan didasarkan pada analisis escape dan penggantian skalar, jadi analisis escape dan penggantian skalar harus diaktifkan. Tentu saja, JDK1.8 diaktifkan secara default.
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
-
-
- 开启标量替换:-XX:+EliminateAllocations
- 关闭标量替换:-XX:-EliminateAllocations
- 显示标量替换详情:-XX:+PrintEliminateAllocations
Contoh alokasi pada stack:
- 示例1
- import java.lang.management.ManagementFactory;
- import java.util.List;
- /**
- * 逃逸分析优化-栈上分配
- * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
- * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
- *虚拟机参数设置如下,表示做了逃逸分析 消耗时间在10毫秒以下
- * -server -Xmx10M -Xms10M
- -XX:+DoEscapeAnalysis -XX:+PrintGC
- *
- *虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
- * -server -Xmx10m -Xms10m
- -XX: -DoEscapeAnalysis -XX:+PrintGC
- * @author 734621
- *
- */
-
- public class OnStack{
- public static void alloc(){
- byte[] b=new byte[2];
- b[0]=1;
- }
- public static void main(String [] args){
- long b=System.currentTimeMillis();
- for(int i=0;i<100000000;i++){
- alloc();
- }
- long e=System.currentTimeMillis();
- System.out.println("消耗时间为:" + (e - b));
- List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
- for(String p : paramters){
- System.out.println(p);
- }
- }
- }
-
-
- 加逃逸分析的结果
- [GC (Allocation Failure) 2816K->484K(9984K), 0.0013117 secs]
- 消耗时间为:7
- -Xmx10m
- -Xms10m
- -XX:+DoEscapeAnalysis
- -XX:+PrintGC
-
-
-
- 没有加逃逸分析的结果如下:
- [GC (Allocation Failure) 3320K->504K(9984K), 0.0003174 secs]
- [GC (Allocation Failure) 3320K->504K(9984K), 0.0002524 secs]
- 消耗时间为:1150
- -Xmx10m
- -Xms10m
- -XX:-DoEscapeAnalysis
- -XX:+PrintGC
-
-
- 以上测试可以看出,栈上分配可以明显提高效率: 效率是不开启的1150/7= 160倍
-
-
- 示例2
- 我们通过举例来说明 开启逃逸分析 和 未开启逃逸分析时候的情况
-
- class User {
- private String name;
- private String age;
- private String gender;
- private String phone;
- }
- public class StackAllocation {
- public static void main(String[] args) throws InterruptedException {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 100000000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- System.out.println("花费的时间为:" + (end - start) + " ms");
-
- // 为了方便查看堆内存中对象个数,线程sleep
- Thread.sleep(10000000);
- }
-
- private static void alloc() {
- // 未发生逃逸
- User user = new User();
- }
- }
- 设置JVM参数,表示未开启逃逸分析
- -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
- 花费的时间为:664 ms
- 然后查看内存的情况,发现有大量的User存储在堆中
-
- 开启逃逸分析
- -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
- 然后查看运行时间,我们能够发现花费的时间快速减少,同时不会发生GC操作
- 花费的时间为:5 ms
- 在看内存情况,我们发现只有很少的User对象,说明User未发生逃逸,因为它存储在栈中,随着栈的销毁而消失。
Sebagai perbandingan, kita bisa melihat
Analisis escape dapat mendeteksi objek tertentu hanya diakses oleh satu thread dan tidak lolos ke thread lain. Oleh karena itu, operasi sinkronisasi yang tidak diperlukan dapat dihilangkan dan overhead eksekusi program multi-thread dapat dikurangi.
Kunci sinkronisasi sangat memakan kinerja, jadi ketika kompiler menentukan bahwa suatu objek tidak lolos, ia akan menghapus kunci sinkronisasi dari objek tersebut. JDK1.8 mengaktifkan kunci sinkronisasi secara default, tetapi ini didasarkan pada pengaktifan analisis escape.
- -XX:+EliminateLocks #开启同步锁消除(JVM默认状态)
- -XX:-EliminateLocks #关闭同步锁消除
- 通过示例: 明显可以看到“逃逸分析和锁消除” 对性能的提升
-
- public void testLock(){
- long t1 = System.currentTimeMillis();
- for (int i = 0; i < 100_000_000; i++) {
- locketMethod();
- }
- long t2 = System.currentTimeMillis();
- System.out.println("耗时:"+(t2-t1));
- }
-
- public static void locketMethod(){
- EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
- synchronized(escapeAnalysis) {
- escapeAnalysis.obj2="abcdefg";
- }
- }
-
- 设置JVM参数,开启逃逸分析, 耗时:
- java -Xmx64m -Xms64m -XX:+DoEscapeAnalysis
-
- 设置JVM参数,关闭逃逸分析, 耗时:
- java -Xmx64m -Xms64m -XX:-DoEscapeAnalysis
-
- 设置JVM参数,关闭锁消除,再次运行
- java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
-
- 设置JVM参数,开启锁消除,再次运行
- java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks
-
-
Biaya sinkronisasi thread cukup tinggi, dan konsekuensi sinkronisasi adalah berkurangnya konkurensi dan kinerja.
Saat mengkompilasi blok tersinkronisasi secara dinamis, kompiler JIT dapat menggunakan analisis escape untuk menentukan apakah objek kunci yang digunakan oleh blok tersinkronisasi hanya dapat diakses oleh satu thread dan belum dilepaskan ke thread lain. Jika tidak, kompiler JIT akan membatalkan sinkronisasi bagian kode ini saat mengkompilasi blok yang disinkronkan ini. Hal ini dapat sangat meningkatkan konkurensi dan kinerja. Proses pembatalan sinkronisasi ini disebut penghilangan sinkronisasi, disebut juga penghapusan kunci.
- 例如下面的代码
-
- public void f() {
- Object hellis = new Object();
- synchronized(hellis) {
- System.out.println(hellis);
- }
- }
- 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
-
- public void f() {
- Object hellis = new Object();
- System.out.println(hellis);
- }
- 我们将其转换成字节码,此处发现,还是有同步锁的身影,是因为优化是在编译阶段的,在加载进内存后发生。
Analisis escape dapat membagi objek menjadi beberapa skalar, seperti tipe primitif atau objek lain, dan menetapkannya di lokasi berbeda. Hal ini dapat mengurangi fragmentasi memori dan overhead akses objek, serta meningkatkan efisiensi pemanfaatan memori.
Pertama-tama, kita harus memahami skalar dan agregat. Referensi pada tipe dan objek dasar dapat dipahami sebagai skalar, dan keduanya tidak dapat diuraikan lebih jauh. Besaran yang dapat diuraikan lebih lanjut adalah besaran agregat, misalnya: benda.
Benda tersebut merupakan besaran agregat, yang selanjutnya dapat diuraikan menjadi skalar dan variabel anggotanya menjadi variabel diskrit.
Dengan cara ini, jika suatu objek tidak lolos, tidak perlu membuatnya sama sekali. Hanya skalar anggota yang digunakannya yang akan dibuat di tumpukan atau register, sehingga menghemat ruang memori dan meningkatkan kinerja aplikasi.
Substitusi skalar juga diaktifkan secara default di JDK1.8, namun juga harus didasarkan pada analisis escape yang diaktifkan.
Skalar adalah data yang tidak dapat dipecah menjadi data yang lebih kecil. Tipe data primitif di Java adalah skalar.
Sebaliknya, data yang dapat didekomposisi disebut agregat. Suatu objek di Java merupakan agregat karena dapat didekomposisi menjadi agregat dan skalar lainnya.
- public static void main(String args[]) {
- alloc();
- }
- class Point {
- private int x;
- private int y;
- }
- private static void alloc() {
- Point point = new Point(1,2);
- System.out.println("point.x" + point.x + ";point.y" + point.y);
- }
- 以上代码,经过标量替换后,就会变成
-
- private static void alloc() {
- int x = 1;
- int y = 2;
- System.out.println("point.x = " + x + "; point.y=" + y);
- }
Pada tahap JIT, jika melalui escape analysis ditemukan bahwa suatu objek tidak dapat diakses oleh dunia luar, maka setelah optimasi JIT, objek tersebut akan dibongkar menjadi beberapa variabel anggota yang terdapat di dalamnya dan diganti. Proses ini adalah penggantian skalar.
Terlihat bahwa setelah dilakukan analisis escape, ditemukan besaran agregat Titik tidak lolos, sehingga digantikan oleh dua skalar. Lalu apa manfaat substitusi skalar? Artinya, ini bisa sangat mengurangi penggunaan memori heap. Karena sekali tidak perlu membuat objek, tidak perlu mengalokasikan memori heap. Substitusi skalar memberikan dasar yang baik untuk alokasi pada tumpukan.
Pengujian analisis pelarian
- 逃逸分析测试
- 代码如下,大致思路就是 for 循环 1 亿次,循环体内调用外部的 allot() 方法,而 allot() 方法的作用就是简单创建一个对象,但是这个对象是内部的,所以是未逃逸的,所以理论上 JVM 是会进行优化的,我们拭目以待。并且我们会对比开启和关闭逃逸分析之后各自程序的运行时间:
-
- /**
- * @ClassName: EscapeAnalysisTest
- * @Description: http://www.jetchen.cn 逃逸分析 demo
- * @Author: Jet.Chen
- * @Date: 2020/11/23 14:26
- * @Version: 1.0
- **/
- public class EscapeAnalysisTest {
-
- public static void main(String[] args) {
- long t1 = System.currentTimeMillis();
- for (int i = 0; i < 100000000; i++) {
- allot();
- }
- long t2 = System.currentTimeMillis();
- System.out.println(t2-t1);
- }
-
- private static void allot() {
- Jet jet = new Jet();
- }
-
- static class Jet {
- public String name;
- }
-
- }
- 上面就是我们进行逃逸分析测试的代码, mian() 方法末尾有一个线程暂停,目的是为了观察此时 JVM 中的内存情况。
-
- Step 1:测试开启逃逸
- 由于环境是 jdk1.8,默认开启了逃逸分析,所以直接运行,得到结果如下,程序耗时 3 毫秒:
-
-
- 此时线程是处于睡眠状态的,我们观察下内存情况,发现堆内存中一共新建了 11 万个 Jet 对象。
-
-
-
- Step 2:测试关闭逃逸
- 我们关闭逃逸分析再来运行一次(使用 java -XX:-DoEscapeAnalysis EscapeAnalysisTest 来运行代码即可),得到结果如下,程序耗时 400 毫秒:
-
-
- 此时我们观察下内存情况,发现堆内存中一共新建了 3 千多万个 Jet 对象。
-
-
- 所以,无论是从代码的执行时间(3 毫秒 VS 400 毫秒),还是从堆内存中对象的数量(11 万个 VS 3 千万个)来分析,在上述场景下,开启逃逸分析是有正向益的。
-
- Step 3:测试标量替换
- 我们测试下开启和关闭 标量替换,如下图:
-
-
- 由上图我们可以看出,在上述极端场景下,开启和关闭标量替换对于性能的影响也是满巨大的,另外,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。
-
- Step 4:测试锁消除
- 测试锁消除,我们需要简单调整下代码,即给 allot() 方法中的内容加锁处理,如下:
-
- private static void allot() {
- Jet jet = new Jet();
- synchronized (jet) {
- jet.name = "jet Chen";
- }
- }
- 然后我们运行测试代码,测试结果也很明显,在上述场景下,开启和关闭锁消除对程序性能的影响也是巨大的。
-
-
- /**
- * 进行两种测试
- * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
- * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 开启逃逸分析 jdk8默认开启
- * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 执行main方法后
- * jps 查看进程
- * jmap -histo 进程ID
- *
- */
- @Slf4j
- public class EscapeTest {
-
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 500000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- log.info("执行时间:" + (end - start) + " ms");
- try {
- Thread.sleep(Integer.MAX_VALUE);
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- }
-
-
- /**
- * JIT编译时会对代码进行逃逸分析
- * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
- * Ponit没有逃逸
- */
- private static String alloc() {
- Point point = new Point();
- return point.toString();
- }
-
- /**
- *同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
- */
- public void append(String str1, String str2) {
- StringBuffer stringBuffer = new StringBuffer();
- stringBuffer.append(str1).append(str2);
- }
-
- /**
- * 标量替换
- *
- */
- private static void test2() {
- Point point = new Point(1,2);
- System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
-
- // int x=1;
- // int y=2;
- // System.out.println("point.x="+x+"; point.y="+y);
- }
-
-
- }
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- class Point{
- private int x;
- private int y;
- }
Analisis escape dapat menentukan bahwa pemanggilan metode tertentu tidak akan lepas dari cakupan metode saat ini. Oleh karena itu, metode ini dapat dioptimalkan secara inline untuk mengurangi biaya pemanggilan metode dan meningkatkan efisiensi eksekusi program.
Melalui strategi pengoptimalan ini, analisis escape dapat membantu JVM mengoptimalkan kode dengan lebih baik, mengurangi overhead pengumpulan sampah, meningkatkan efisiensi dan daya tanggap eksekusi program, dan mengurangi penggunaan memori.
Analisis escape memiliki beragam skenario aplikasi dalam aplikasi Java sebenarnya. Berikut ini adalah beberapa skenario aplikasi umum:
Makalah tentang analisis pelarian diterbitkan pada tahun 1999, tetapi baru diimplementasikan pada JDK1.6, dan teknologi ini belum terlalu matang.
Alasan mendasarnya adalah tidak ada jaminan bahwa konsumsi kinerja analisis escape akan lebih tinggi dibandingkan konsumsinya. Meskipun analisis escape dapat melakukan substitusi skalar, alokasi tumpukan, dan eliminasi kunci. Namun, analisis pelepasan diri itu sendiri juga memerlukan serangkaian analisis yang kompleks, yang sebenarnya merupakan proses yang memakan waktu relatif lama.
Contoh ekstrimnya adalah setelah analisis escape, ditemukan bahwa tidak ada objek yang tidak lolos. Maka proses analisis escape menjadi sia-sia.
Meskipun teknologi ini belum terlalu matang, ini juga merupakan sarana yang sangat penting dalam teknologi optimasi kompiler just-in-time. Saya perhatikan ada beberapa pendapat bahwa melalui analisis escape, JVM akan mengalokasikan objek di tumpukan yang tidak akan lolos. Hal ini secara teoritis mungkin, tetapi itu tergantung pada pilihan desainer JvM. Sejauh yang saya tahu, Oracle Hotspot JVM tidak melakukan ini. Hal ini telah dijelaskan dalam dokumen terkait analisis escape, jadi jelas bahwa semua instance objek dibuat di heap.
Saat ini, banyak buku yang masih didasarkan pada versi sebelum JDK7. JDK telah mengalami perubahan besar. Cache string internal dan variabel statis pernah dialokasikan pada generasi permanen, dan generasi permanen telah digantikan oleh area metadata. Namun, cache string internal dan variabel statis tidak ditransfer ke area metadata, tetapi dialokasikan langsung di heap, jadi ini juga konsisten dengan kesimpulan dari poin sebelumnya: instance objek dialokasikan di heap. Contoh di atas dipercepat karena substitusi skalar.
Jika suatu objek tidak lolos dalam badan metode atau dalam thread (atau ditentukan bahwa objek tersebut gagal lolos setelah analisis escape), optimasi berikut dapat dilakukan:
Dalam keadaan normal, objek yang tidak dapat keluar menempati ruang yang relatif besar. Jika ruang pada tumpukan dapat digunakan, sejumlah besar objek akan hancur saat metode berakhir, sehingga mengurangi tekanan GC.
Jika ada kunci sinkronisasi pada metode kelas yang Anda tentukan, tetapi hanya satu thread yang mengaksesnya saat runtime, kode mesin setelah analisis escape akan berjalan tanpa kunci sinkronisasi.
Tipe data primitif di mesin virtual Java (tipe numerik seperti int, long, dan tipe referensi, dll.) tidak dapat didekomposisi lebih lanjut, dan dapat disebut skalar. Sebaliknya, jika suatu bagian data dapat terus didekomposisi, maka disebut agregat. Agregat paling umum di Java adalah objek. Jika analisis escape membuktikan bahwa suatu objek tidak akan diakses secara eksternal dan objek tersebut dapat didekomposisi, objek tersebut tidak boleh dibuat saat program benar-benar dijalankan, melainkan secara langsung membuat beberapa variabel anggotanya yang digunakan oleh metode ini untuk menggantikannya. Variabel yang dibongkar dapat dianalisis dan dioptimalkan secara terpisah. Setelah atribut diratakan, tidak perlu menjalin hubungan melalui penunjuk referensi. Variabel tersebut dapat disimpan secara terus menerus dan kompak, yang lebih ramah terhadap berbagai penyimpanan dan menghemat banyak penanganan data selama eksekusi. menyebabkan hilangnya kinerja. Pada saat yang sama, Anda juga dapat mengalokasikan ruang pada frame tumpukan atau register masing-masing, sehingga objek asli tidak perlu mengalokasikan ruang secara keseluruhan.
Generasi muda adalah tempat lahir, tumbuh, dan matinya suatu benda. Suatu benda dihasilkan dan digunakan di sini, dan akhirnya dikumpulkan oleh pemulung dan mengakhiri masa pakainya.
Objek dengan siklus hidup yang panjang yang ditempatkan pada generasi lama biasanya merupakan objek Java yang disalin dari area penyintas. Tentu saja, ada juga kasus khusus. Kita tahu bahwa objek biasa akan dialokasikan di TLAB; jika objeknya besar, JVM akan mencoba mengalokasikannya langsung ke lokasi lain di Eden; dapat menemukan ruang kosong terus menerus yang cukup lama di ruang generasi baru, JVM akan langsung mengalokasikannya ke generasi lama. Jika GC hanya terjadi pada generasi muda, maka tindakan mendaur ulang objek generasi muda disebut MinorGc.
Jika GC terjadi pada generasi lama disebut MajorGc atau FullGC. Secara umum frekuensi kemunculan MinorGc jauh lebih tinggi dibandingkan MajorGC, artinya frekuensi pengumpulan sampah pada generasi tua akan jauh lebih rendah dibandingkan pada generasi muda.
Analisis escape JVM menggunakan dua metode analisis, statis dan dinamis, untuk menentukan apakah suatu objek dapat keluar dari cakupan suatu metode. Ini dapat membantu JVM mengoptimalkan kode dan meningkatkan kinerja dan efisiensi penggunaan memori program Java.
Strategi optimasi untuk analisis escape meliputi alokasi on-stack, eliminasi sinkronisasi, substitusi skalar, dan penyelarasan metode. Strategi pengoptimalan ini dapat mengurangi overhead pengumpulan sampah, meningkatkan efisiensi dan daya tanggap eksekusi program, serta mengurangi penggunaan memori.
mengacu pada:
https://zhuanlan.zhihu.com/p/693382698
JVM-Heap-Escape Analysis-08-CSDN Blog
Analisis pelarian memori JIT_java mematikan Blog CSDN pengganti skalar
java -XX:+PrintFlagsFinal #输出打印所有参数jvm参数