Cara Kerja Java

Memahami JVM, bytecode, dan JIT beserta makna nyata slogan write once, run anywhere.

Daftar isi

Program Java menempuh perjalanan panjang sebelum teks yang ditulis pengembang benar-benar dieksekusi prosesor. Topik ini menguraikan perjalanan tersebut, dari file kode sumber, proses kompilasi menjadi bytecode, sampai pekerjaan Java Virtual Machine yang menjalankannya. Setelah membacanya, slogan write once, run anywhere akan terasa konkret, lengkap dengan batas-batasnya.

Dari kode sumber ke bytecode#

Kode Java disimpan sebagai file teks biasa berekstensi .java. Isinya masih terbaca manusia, tetapi prosesor tidak mengerti bentuk seperti itu. Sebelum dijalankan, file .java diterjemahkan oleh compiler bernama javac menjadi file .class yang berisi bytecode, yakni rangkaian instruksi untuk Java Virtual Machine (JVM), bukan untuk prosesor tertentu. Instruksi bytecode didefinisikan ketat oleh spesifikasi JVM, misalnya memuat nilai ke tumpukan, menyimpannya ke variabel lokal, atau memanggil metode.

Karena bytecode tidak mengenal sistem operasi, file .class yang dihasilkan javac di Linux sama saja dengan yang dihasilkan di Windows. Yang beradaptasi dengan tiap sistem adalah JVM, yang punya implementasi berbeda untuk setiap platform. Fondasi teknis slogan write once, run anywhere ada di sini.

Sejak JDK 11 tersedia jalan pintas untuk belajar, yaitu mode satu file sumber. Perintah java Halo.java mengompilasi file itu di memori lalu langsung menjalankannya, tanpa meninggalkan file .class di folder. Proyek sungguhan dengan banyak file dan library tetap memakai javac atau alat build, tetapi untuk contoh kecil mode ini jauh lebih praktis.

Halo.javakode sumberjavacHalo.classbytecodedimuatJVMinterpreter + JIT,garbage collectorkode mesinProsesorinstruksi mesin
Perjalanan program Java dari kode sumber sampai prosesor

Pekerjaan JVM saat program berjalan#

JVM adalah program yang bertugas mengeksekusi bytecode. Sebutan ini sering dipakai untuk dua hal sekaligus, yaitu spesifikasi yang menetapkan aturannya dan implementasi nyata yang berjalan di komputer. Implementasi paling umum bernama HotSpot, menjadi bawaan OpenJDK dan Oracle JDK, dan membawa pula compiler JIT yang dibahas sebentar lagi.

Saat perintah java Halo dijalankan, beberapa komponen JVM bekerja berurutan. Class loader mencari dan memuat file .class yang dibutuhkan, termasuk yang berasal dari library standar, baru pada saat benar-benar diperlukan. Verifier kemudian memeriksa bytecode-nya agar tidak ada instruksi yang merusak memori, misalnya membaca alamat sembarangan. Setelah lolos, instruksi dieksekusi, dan garbage collector membersihkan objek yang tidak terpakai lagi sehingga pengelolaan memori tidak dilakukan manual.

Model ini berbeda dari bahasa yang hasil kompilasinya langsung menjadi kode mesin seperti C. Program Java membawa lapisan yang bisa memeriksa dan mengelola dirinya sendiri, dan lapisan itulah yang membuat fitur seperti pemulihan memori otomatis serta pemeriksaan keamanan bytecode mungkin diterapkan.

Interpreter dan JIT#

JVM mengeksekusi bytecode dengan dua cara sekaligus. Di awal, instruksi dijalankan satu per satu oleh interpreter, sehingga eksekusi tergolong lambat. Di saat yang sama, HotSpot memantau bagian kode yang paling sering dieksekusi, yang dijuluki hot spot dan menjadi asal nama implementasinya. Compiler JIT, singkatan dari just-in-time, lalu menerjemahkan bagian panas itu menjadi kode mesin saat program masih berjalan, sehingga eksekusi berikutnya memakai kode mesin yang jauh lebih cepat.

Akibatnya program Java berperilaku khas, yaitu lebih lambat saat baru dimulai dan makin cepat setelah menghangat. Perilaku ini bisa diamati dengan program yang mengukur waktu tiga percobaan pada metode yang sama:

java PemanasanJit.java
// tidak-dijalankan
public class PemanasanJit {

    static long hitungJumlah(int putaran) {
        long jumlah = 0;
        for (int i = 1; i <= putaran; i++) {
            jumlah += i;
        }
        return jumlah;
    }

    public static void main(String[] args) {
        int putaran = 200_000_000;
        for (int percobaan = 1; percobaan <= 3; percobaan++) {
            long mulai = System.nanoTime();
            long jumlah = hitungJumlah(putaran);
            long durasiMs = (System.nanoTime() - mulai) / 1_000_000;
            System.out.println("percobaan " + percobaan + ": " + durasiMs + " ms, jumlah = " + jumlah);
        }
    }
}

Keluaran program ini sengaja tidak ditempel, karena angka waktunya berubah pada setiap eksekusi dan bergantung pada mesin yang menjalankannya. Pola yang biasa muncul adalah percobaan pertama paling lambat, lalu percobaan berikutnya jauh lebih cepat setelah metode hitungJumlah dikompilasi JIT. Program di atas boleh langsung dijalankan di mesin sendiri untuk melihat polanya.

Makna write once, run anywhere#

Program dapat mengetahui lingkungan tempatnya berjalan lewat properti sistem yang disediakan JVM. Program berikut mencetak versi Java, nama implementasi JVM, serta sistem operasi dan arsitektur tempatnya dieksekusi.

java CekLingkungan.java
public class CekLingkungan {

    public static void main(String[] args) {
        System.out.println("java.version = " + System.getProperty("java.version"));
        System.out.println("java.vm.name = " + System.getProperty("java.vm.name"));
        System.out.println("os.name      = " + System.getProperty("os.name"));
        System.out.println("os.arch      = " + System.getProperty("os.arch"));
    }
}
text Keluaran
java.version = 21.0.13
java.vm.name = OpenJDK 64-Bit Server VM
os.name      = Linux
os.arch      = amd64

Kode di atas tidak memuat satu pun hal yang spesifik ke Linux. Bila file yang sama dijalankan di Windows atau macOS, baris os.name dan os.arch berubah mengikuti tempatnya, sedangkan kodenya tetap. Itulah wujud paling nyata dari write once, run anywhere, yakni bytecode yang sama dieksekusi oleh JVM yang berbeda-beda.

Janji itu punya batas yang perlu diketahui sejak awal. Library native berbentuk .dll atau .so memecah portabilitas karena harus disiapkan per platform. Tampilan GUI antar sistem operasi bisa berbeda detailnya. Versi JVM juga menentukan, karena file .class hasil JDK baru memuat bytecode yang tidak dikenal JVM lama. Program yang dikompilasi untuk JDK lama tetap berjalan di JVM yang lebih baru, tetapi jalan sebaliknya tidak.

Menyibak bytecode dengan javap#

JDK menyertakan alat kecil bernama javap yang menampilkan isi file .class. Dengan opsi -c, javap mencetak bytecode setiap metode baris demi baris, sehingga kotak bytecode pada diagram alur tadi dapat dilihat langsung. Alat ini membaca file .class yang sudah ada, bukan kode sumbernya, sehingga file tersebut harus dihasilkan javac terlebih dahulu.

Urutan perintahnya dua langkah. Berikut bentuknya pada program CekLingkungan yang sudah dibuat di bagian sebelumnya.

bash
# tidak-dijalankan
# javac dan javap ikut terpasang bersama JDK lengkap di mesin pembaca
javac CekLingkungan.java
javap -c CekLingkungan

Hasilnya berupa daftar instruksi pendek seperti memuat konstanta, memuat variabel lokal, dan memanggil metode, dengan jumlahnya jauh lebih banyak daripada baris kode sumber aslinya. Membaca bytecode langsung jarang diperlukan dalam pekerjaan sehari-hari, tetapi berguna saat ingin memahami cara JVM bekerja atau membedah perilaku yang tidak terduga.

Perbedaan JVM, JRE, dan JDK#

Tiga istilah ini sering tertukar karena saling bersarang. JVM adalah mesin eksekusinya, JRE adalah JVM ditambah library standar untuk menjalankan program, sedangkan JDK adalah JRE ditambah peralatan pengembang seperti javac, javap, dan debugger.

IstilahIsiDipakai oleh
JVMmesin yang mengeksekusi bytecodesemua program Java, di balik layar
JREJVM ditambah library standarorang yang hanya menjalankan program
JDKJRE ditambah alat pengembangpengembang yang menulis dan mengompilasi kode

Dalam praktik hari ini, paket JRE terpisah tidak lagi disediakan oleh Oracle sejak JDK 11. Distribusi JDK modern seperti Temurin dan Oracle JDK mengemas semuanya dalam satu unduhan, dan alat seperti jlink bahkan bisa merakit runtime yang isinya dikecilkan sesuai kebutuhan. Istilah JRE tetap berguna untuk memahami peran tiap komponen, tetapi saat memasang Java sekarang, yang diunduh adalah JDK. Memahami susunan ini juga menjelaskan satu pesan error yang umum, yaitu perintah javac yang tidak ditemukan biasanya berarti yang terpasang hanya runtime.

Kesalahan umum#

Menyimpulkan Java lambat dari program yang hangat perlahan. Reputasi lambat berasal dari era 1990-an ketika bytecode benar-benar hanya diinterpretasi tanpa kompilasi. HotSpot modern mengompilasi kode yang panas menjadi kode mesin, sehingga untuk banyak beban kerja server kecepatannya setara bahasa yang dikompilasi statis. Kesimpulan yang lebih adil adalah Java lebih lambat saat memulai dan cepat saat stabil.

Mengira file .class adalah program yang dijalankan langsung oleh sistem operasi. File .class berisi bytecode untuk JVM, bukan kode mesin, sehingga file itu tidak menghasilkan apa pun bila dibuka dua kali dari file manager. Cara menjalankannya adalah lewat launcher java, misalnya java Halo bila Halo.class ada di folder, atau java Halo.java untuk mode satu file. Untuk membagikan program yang terdiri atas banyak file, pengembang umumnya membungkusnya menjadi satu file .jar yang juga dipanggil lewat perintah java.

Ringkasan#

  • javac menerjemahkan file .java menjadi bytecode .class yang berisi instruksi untuk JVM, bukan untuk prosesor tertentu.
  • JVM memuat dan memverifikasi bytecode lewat class loader dan verifier, mengeksekusinya lewat interpreter, lalu JIT mengompilasi bagian terpanas menjadi kode mesin.
  • write once, run anywhere berarti bytecode yang sama berjalan di platform mana pun yang memiliki JVM, dengan batasan pada library native, GUI, dan versi JVM.
  • JVM adalah mesin eksekusinya, JRE menambahkan library standar, dan JDK menambahkan alat pengembang.

Referensi#