Java HotSpot JIT Compiler - Device, Monitoring, dan Tuning (Bagian 1)

Compiler JIT (Just-in-Time) memiliki dampak yang sangat besar pada kinerja aplikasi. Memahami cara kerjanya, cara memantau dan mengkonfigurasinya adalah penting untuk setiap programmer Java. Dalam seri artikel dua bagian ini, kita akan melihat compiler JIT di HotSpot JVM, cara memantau operasinya, dan cara mengkonfigurasinya. Pada bagian pertama ini, kita akan melihat bagaimana kompilator JIT bekerja dan bagaimana ia dapat dimonitor.



Kompiler AOT dan JIT



Prosesor hanya dapat menjalankan sekumpulan instruksi terbatas - kode mesin. Untuk program yang akan dijalankan oleh prosesor, itu harus direpresentasikan sebagai kode mesin.



Ada bahasa pemrograman yang dikompilasi seperti C dan C ++. Program yang ditulis dalam bahasa ini didistribusikan sebagai kode mesin. Setelah program ditulis, proses khusus - kompiler Ahead-of-Time (AOT), biasanya disebut hanya sebagai kompilator, menerjemahkan kode sumber menjadi kode mesin. Kode mesin dirancang untuk dijalankan pada model prosesor tertentu. Prosesor dengan arsitektur umum dapat menjalankan kode yang sama. Model prosesor yang lebih baru umumnya mendukung instruksi dari model sebelumnya, tetapi tidak sebaliknya. Misalnya, kode mesin yang menggunakan instruksi AVX untuk prosesor Intel Sandy Bridge tidak dapat berjalan pada prosesor Intel yang lebih lama. Ada berbagai cara untuk mengatasi masalah ini, misalnya, mentransfer bagian penting dari program ke pustaka yang memiliki versi untuk model prosesor utama.Tetapi seringkali program hanya dikompilasi untuk model prosesor yang relatif lama dan tidak memanfaatkan set instruksi baru.



Berbeda dengan bahasa pemrograman yang dikompilasi, ada bahasa yang ditafsirkan seperti Perl dan PHP. Dengan pendekatan ini, kode sumber yang sama dapat dijalankan pada platform apa pun yang memiliki interpreter. Kelemahan dari pendekatan ini adalah bahwa kode yang ditafsirkan lebih lambat daripada kode mesin yang melakukan hal yang sama.



Bahasa Java menawarkan pendekatan yang berbeda, persilangan antara bahasa yang dikompilasi dan ditafsirkan. Aplikasi Java dikompilasi menjadi kode tingkat rendah menengah - bytecode.

Nama bytecode dipilih karena tepat satu byte digunakan untuk menyandikan setiap operasi. Ada sekitar 200 operasi di Java 10.



Bytecode kemudian dijalankan oleh JVM serta program bahasa yang diinterpretasikan. Tetapi karena bytecode memiliki format yang terdefinisi dengan baik, JVM dapat mengkompilasinya ke kode mesin saat runtime. Biasanya, versi JVM yang lebih lama tidak akan dapat menghasilkan kode mesin menggunakan set instruksi prosesor yang baru setelahnya. Di sisi lain, untuk mempercepat program Java, program ini bahkan tidak perlu dikompilasi ulang. Ini cukup untuk menjalankannya di JVM yang lebih baru.



Kompiler JIT HotSpot



Implementasi JIT JVM yang berbeda dapat mengimplementasikan compiler dengan cara yang berbeda. Pada artikel ini, kita melihat Oracle HotSpot JVM dan implementasi compiler JIT-nya. Nama HotSpot berasal dari pendekatan yang digunakan JVM untuk mengompilasi bytecode. Biasanya dalam sebuah aplikasi, hanya sebagian kecil dari kode yang dieksekusi cukup sering, dan kinerja aplikasi sangat bergantung pada kecepatan eksekusi bagian tertentu ini. Bagian kode ini disebut hot spot dan merupakan kompilasi JIT. Beberapa penilaian mendasari pendekatan ini. Jika kode hanya dijalankan satu kali, maka kompilasi kode itu membuang-buang waktu. Alasan lainnya adalah pengoptimalan. Semakin sering JVM mengeksekusi kode apa pun, semakin banyak statistik yang diakumulasikannya, yang dengannya Anda dapat menghasilkan kode yang lebih dioptimalkan.Selain itu, compiler membagikan resource mesin virtual dengan aplikasi itu sendiri, sehingga resource yang digunakan untuk pembuatan profil dan pengoptimalan dapat digunakan untuk menjalankan aplikasi itu sendiri, yang memaksa keseimbangan tertentu untuk diamati. Unit kerja untuk kompiler HotSpot adalah metode dan loop.

Unit kode yang dikompilasi disebut nmethod (kependekan dari metode asli).



Kompilasi berjenjang



Nyatanya, HotSpot JVM tidak hanya memiliki satu, tetapi dua kompiler: C1 dan C2. Nama lain mereka adalah klien (klien) dan server (server). Secara historis C1 digunakan dalam aplikasi GUI dan C2 dalam aplikasi server. Penyusun berbeda dalam hal seberapa cepat mereka mulai mengompilasi kode. C1 mulai mengkompilasi kode lebih cepat, sedangkan C2 dapat menghasilkan kode yang lebih optimal.



Di versi JVM sebelumnya, Anda harus memilih kompiler menggunakan tanda -client untuk klien dan -server atau -d64untuk ruang server. JDK 6 memperkenalkan mode kompilasi multi-tier. Secara kasar, esensinya terletak pada transisi berurutan dari kode yang diinterpretasikan ke kode yang dihasilkan oleh compiler C1, dan kemudian C2. Di JDK 8 flag -client, -server, dan -d64 diabaikan, dan di JDK 11 flag -d64 telah dihapus dan menghasilkan error. Anda dapat mematikan mode kompilasi berjenjang dengan tanda -XX: -TieredCompilation .



Ada 5 level kompilasi:



  • 0 - kode yang ditafsirkan
  • 1 - C1 dioptimalkan sepenuhnya (tanpa profil)
  • 2 - C1 dengan mempertimbangkan jumlah panggilan metode dan iterasi loop
  • 3 - C1 dengan profiling
  • 4 - C2


Urutan transisi khas antar level ditampilkan dalam tabel.

Urutan

Deskripsi

0-3-4 Penerjemah, level 3, level 4. Paling umum.
0-2-3-4 , 4 (C2) . 2. , 3 , , 4.
0-2-4 , 3 . 4 3. 2 4.
0-3-1 . 3, , 4 . 1.
0-4 .


Code cache



Kode mesin yang dikompilasi oleh kompilator JIT disimpan di area memori yang disebut cache kode. Ini juga menyimpan kode mesin dari mesin virtual itu sendiri, seperti kode interpreter. Ukuran area memori ini terbatas, dan jika sudah penuh, kompilasi berhenti. Dalam kasus ini, beberapa metode "panas" akan terus dijalankan oleh penerjemah. Jika terjadi overflow, JVM menampilkan pesan berikut:



Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full.
         Compiler has been disabled.

      
      





Cara lain untuk mengetahui tentang luapan area memori ini adalah dengan mengaktifkan pencatatan kompiler (cara melakukannya dibahas di bawah).

Cache kode dapat dikonfigurasi dengan cara yang sama seperti area memori lain di JVM. Ukuran awal ditentukan oleh parameter -XX: InitialCodeCacheSize . Ukuran maksimum ditentukan oleh parameter -XX: ReservedCodeCacheSize . Secara default, ukuran awal adalah 2496 KB. Ukuran maksimum adalah 48 MB saat kompilasi berjenjang nonaktif dan 240 MB saat aktif.



Sejak Java 9, cache kode dibagi menjadi 3 segmen (ukuran total masih dibatasi oleh batasan yang dijelaskan di atas):



  • JVM internal (non-method code). , JVM, , . . 5.5 MB. -XX:NonNMethodCodeHeapSize.
  • Profiled code. . non-method code . 21.2 MB 117.2 MB . -XX:ProfiledCodeHeapSize.
  • Non-profiled code. . non-method code . 21.2 MB 117.2 MB . -XX: NonProfiledCodeHeapSize.




Anda dapat mengaktifkan pencatatan proses kompilasi dengan tanda -XX: + PrintCompilation (ini dinonaktifkan secara default). Jika flag ini disetel, JVM akan menulis pesan ke output standar (STDOUT) setiap kali metode atau loop dikompilasi. Sebagian besar pesan memiliki format berikut: atribut timestamp compilation_id tiered_level method_name size deopt.



Bidang cap waktu adalah waktu sejak dimulainya JVM.



Bidang compilation_id adalah ID internal masalah. Biasanya tumbuh secara berurutan dengan setiap pesan, tetapi terkadang urutannya bisa rusak. Ini bisa terjadi jika ada beberapa utas kompilasi yang berjalan secara paralel.



Bidang atribut adalah sekumpulan lima karakter yang membawa informasi tambahan tentang kode yang dikompilasi. Jika salah satu atribut tidak dapat diterapkan, spasi akan ditampilkan. Atribut berikut ada:



  • % - OSR (penggantian on-stack);
  • s - metode disinkronkan;
  • ! - metode berisi penangan pengecualian;
  • b - kompilasi terjadi dalam mode pemblokiran;
  • n - metode yang dikompilasi adalah pembungkus untuk metode asli.


OSR adalah singkatan dari on-stack replacement. Kompilasi adalah proses yang tidak sinkron. Ketika JVM memutuskan bahwa metode perlu dikompilasi, itu mengantri. Saat metode sedang dikompilasi, JVM terus mengeksekusinya oleh interpreter. Saat metode dipanggil lagi, versi yang dikompilasinya akan dijalankan. Dalam kasus siklus yang panjang, menunggu penyelesaian metode tidak praktis - mungkin tidak selesai sama sekali. JVM mengompilasi badan loop dan harus mulai mengeksekusi versi yang telah dikompilasinya. JVM menyimpan status utas di tumpukan. Untuk setiap metode yang dipanggil, objek Stack Frame baru dibuat di tumpukan, yang menyimpan parameter metode, variabel lokal, nilai kembali, dan nilai lainnya. Selama OSR, Stack Frame baru dibuat untuk menggantikan yang sebelumnya.







Sumber: Penyusun Klien Mesin Virtual Java HotSpotTM: Teknologi dan Aplikasi

Atribut "s" dan "!" Saya pikir mereka tidak membutuhkan penjelasan.



Atribut "b" berarti kompilasi tidak dilakukan di latar belakang, dan tidak boleh ditemukan di versi modern JVM.



Atribut "n" berarti bahwa metode yang dikompilasi adalah pembungkus di sekitar metode asli.

Bidang tiered_level berisi nomor level di mana kode dikompilasi atau dapat dikosongkan jika kompilasi berjenjang dinonaktifkan.



Kolom method_name berisi nama metode yang dikompilasi atau nama metode yang berisi loop yang dikompilasi.



Bidang ukuran berisi ukuran bytecode yang dikompilasi, bukan ukuran kode mesin yang dihasilkan. Ukurannya dalam byte.



Bidang deopt tidak muncul di setiap pesan, ini berisi nama deoptimization yang dilakukan dan mungkin berisi pesan seperti "made not entrant" dan "made zombie".

Terkadang entri berikut mungkin muncul di log: timestamp compile_id COMPILE SKIPPED: reason. Artinya ada yang tidak beres saat metode dikompilasi. Ada kalanya hal ini diharapkan:



  • Cache kode terisi - perlu untuk meningkatkan ukuran area memori cache kode.
  • Pemuatan kelas bersamaan - kelas telah dimodifikasi pada waktu kompilasi.


Dalam semua kasus, kecuali untuk kode cache overflow, JVM akan mencoba untuk melakukan kompilasi ulang. Jika tidak, Anda dapat mencoba menyederhanakan kode.



Jika proses dimulai tanpa tanda -XX: + PrintCompilation, Anda dapat melihat proses kompilasi menggunakan utilitas jstat . Jstat memiliki dua opsi untuk menampilkan informasi kompilasi.



Parameter -compiler menampilkan ringkasan operasi compiler (5003 adalah ID proses):



% jstat -compiler 5003
Compiled Failed Invalid   Time   FailedType FailedMethod
     206         0          0    1.97                0

      
      





Perintah ini juga menampilkan jumlah metode yang gagal dikompilasi dan nama metode terakhir tersebut.



Parameter -printcompilation mencetak informasi tentang metode kompilasi terakhir. Dikombinasikan dengan parameter kedua, periode pengulangan operasi, Anda dapat mengamati proses kompilasi dari waktu ke waktu. Contoh berikut menjalankan perintah -printcompilation every second (1000ms):



% jstat -printcompilation 5003 1000
Compiled  Size  Type Method
     207     64    1 java/lang/CharacterDataLatin1 toUpperCase
     208      5    1 java/math/BigDecimal$StringBuilderHelper getCharArray

      
      





Rencana untuk bagian kedua



Pada bagian selanjutnya, kita akan melihat ambang penghitung di mana JVM mulai mengkompilasi dan bagaimana Anda dapat mengubahnya. Kita juga akan melihat bagaimana JVM memilih jumlah utas kompilator, bagaimana Anda dapat mengubahnya, dan kapan Anda harus melakukannya. Terakhir, mari kita lihat sekilas beberapa pengoptimalan yang dilakukan oleh compiler JIT.



Referensi dan tautan






All Articles