Java Virtual Machine - Kompiler JIT

Dalam bab ini, kita akan belajar tentang kompilator JIT, dan perbedaan antara bahasa yang dikompilasi dan ditafsirkan.

Bahasa yang Dikompilasi vs.

Bahasa seperti C, C ++ dan FORTRAN adalah bahasa yang dikompilasi. Kode mereka dikirimkan sebagai kode biner yang ditargetkan ke mesin yang mendasarinya. Ini berarti bahwa kode tingkat tinggi dikompilasi menjadi kode biner sekaligus oleh kompilator statis yang ditulis khusus untuk arsitektur yang mendasarinya. Biner yang dihasilkan tidak akan berjalan di arsitektur lain.

Di sisi lain, bahasa yang diinterpretasikan seperti Python dan Perl dapat berjalan di mesin apa pun, selama mereka memiliki interpreter yang valid. Ini melewati baris demi baris di atas kode tingkat tinggi, mengubahnya menjadi kode biner.

Kode yang ditafsirkan biasanya lebih lambat dari kode yang dikompilasi. Misalnya, pertimbangkan satu putaran. Sebuah interpreted akan mengubah kode yang sesuai untuk setiap iterasi loop. Di sisi lain, kode yang dikompilasi hanya akan membuat terjemahan menjadi satu. Lebih lanjut, karena interpreter hanya melihat satu baris pada satu waktu, mereka tidak dapat melakukan kode penting seperti, mengubah urutan eksekusi pernyataan seperti compiler.

Kami akan melihat contoh pengoptimalan seperti itu di bawah -

Adding two numbers stored in memory. Karena mengakses memori dapat menghabiskan banyak siklus CPU, kompilator yang baik akan mengeluarkan instruksi untuk mengambil data dari memori dan mengeksekusi penambahan hanya jika data tersedia. Itu tidak akan menunggu dan sementara itu, jalankan instruksi lain. Di sisi lain, pengoptimalan seperti itu tidak mungkin dilakukan selama interpretasi karena penerjemah tidak mengetahui seluruh kode pada waktu tertentu.

Tapi kemudian, bahasa yang diinterpretasikan dapat berjalan di mesin apa pun yang memiliki interpreter yang valid untuk bahasa tersebut.

Apakah Java Dikompilasi atau Ditafsirkan?

Java mencoba mencari jalan tengah. Karena JVM berada di antara kompilator javac dan perangkat keras yang mendasarinya, kompilator javac (atau kompilator lainnya) mengkompilasi kode Java dalam Bytecode, yang dipahami oleh JVM khusus platform. JVM kemudian mengkompilasi Bytecode dalam biner menggunakan kompilasi JIT (Just-in-time), saat kode dijalankan.

HotSpots

Dalam program biasa, hanya ada sebagian kecil kode yang sering dieksekusi, dan seringkali, kode inilah yang memengaruhi kinerja keseluruhan aplikasi secara signifikan. Bagian kode seperti itu disebutHotSpots.

Jika beberapa bagian kode dijalankan hanya sekali, maka kompilasi itu akan membuang-buang tenaga, dan akan lebih cepat untuk menafsirkan Bytecode sebagai gantinya. Tetapi jika bagian tersebut adalah bagian yang panas dan dijalankan beberapa kali, JVM akan mengkompilasinya. Misalnya, jika sebuah metode dipanggil beberapa kali, siklus tambahan yang diperlukan untuk mengompilasi kode akan diimbangi dengan biner yang lebih cepat yang dihasilkan.

Lebih lanjut, semakin JVM menjalankan metode atau loop tertentu, semakin banyak informasi yang dikumpulkannya untuk membuat berbagai pengoptimalan sehingga biner yang lebih cepat dihasilkan.

Mari kita perhatikan kode berikut -

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

Jika kode ini diinterpretasikan, interpreter akan menyimpulkan untuk setiap iterasi bahwa kelas obj1. Ini karena setiap kelas di Java memiliki metode .equals (), yang diperluas dari kelas Object dan bisa diganti. Jadi, meskipun obj1 adalah string untuk setiap iterasi, deduksi akan tetap dilakukan.

Di sisi lain, apa yang sebenarnya akan terjadi adalah bahwa JVM akan memperhatikan bahwa untuk setiap iterasi, obj1 adalah kelas String dan karenanya, itu akan menghasilkan kode yang sesuai dengan metode .equals () dari kelas String secara langsung. Jadi, tidak diperlukan pencarian, dan kode yang dikompilasi akan dieksekusi lebih cepat.

Jenis perilaku ini hanya mungkin jika JVM mengetahui bagaimana kode berperilaku. Jadi, ia menunggu sebelum menyusun bagian tertentu dari kode.

Di bawah ini adalah contoh lain -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

Seorang interpreter, untuk setiap loop, mengambil nilai 'sum' dari memori, menambahkan 'I' padanya, dan menyimpannya kembali ke dalam memori. Akses memori adalah operasi yang mahal dan biasanya membutuhkan banyak siklus CPU. Karena kode ini berjalan beberapa kali, ini adalah HotSpot. JIT akan mengkompilasi kode ini dan melakukan pengoptimalan berikut.

Salinan lokal 'jumlah' akan disimpan dalam register, khusus untuk utas tertentu. Semua operasi akan dilakukan ke nilai di register dan ketika loop selesai, nilainya akan ditulis kembali ke memori.

Bagaimana jika utas lain mengakses variabel juga? Karena pembaruan sedang dilakukan pada salinan lokal variabel oleh beberapa utas lain, mereka akan melihat nilai usang. Sinkronisasi benang diperlukan dalam kasus seperti itu. Primitif sinkronisasi yang paling dasar adalah dengan menyatakan 'sum' sebagai volatile. Sekarang, sebelum mengakses variabel, sebuah thread akan membersihkan register lokalnya dan mengambil nilainya dari memori. Setelah mengaksesnya, nilainya segera ditulis ke memori.

Di bawah ini adalah beberapa pengoptimalan umum yang dilakukan oleh penyusun JIT -

  • Metode inlining
  • Penghapusan kode mati
  • Heuristik untuk mengoptimalkan situs panggilan
  • Lipat konstan