Compiler javac tidak menerjemahkan Java menjadi bahasa mesin prosesor — keluarannya adalah bytecode, instruksi untuk mesin virtual yang didefinisikan spesifikasi JVM. Memahami bytecode membebaskan kita dari tebakan: kita bisa melihat apa yang sebenarnya dihasilkan compiler saat menulis string concatenation, autoboxing, atau loop. Bahasa lain di atas JVM seperti Kotlin, Scala, dan Groovy juga menghasilkan bytecode dengan format yang sama — bytecode adalah bahasa pengantar seluruh ekosistem JVM. Pada chapter ini kita membaca berkas .class dari dalam ke luar, lalu membedah instruksi hasil kompilasi program kecil dengan alat javap.
Anatomi Berkas .class #
Berkas .class berformat biner yang strukturnya baku sejak Java pertama. Bagian pembuka berisi nomor magic 0xCAFEBABE sebagai tanda pengenal format, lalu nomor versi berkas: minor version dan major version. Setiap rilis Java menaikkan major version — JDK 25 menghasilkan major version 69. JVM hanya menerima berkas dengan versi tidak lebih baru dari kemampuannya, sehingga berkas hasil javac 25 ditolak oleh JVM 21 dengan pesan UnsupportedClassVersionError.
Setelah header, struktur utama berkas berjalan berurutan: constant pool — kumpulan konstanta seperti teks string, nama class, dan deskriptor method — kemudian daftar field, daftar method beserta bytecode-nya, dan atribut tambahan. Constant pool berperan sebagai kamus bersama: instruksi bytecode tidak menuliskan nama java/lang/System.out secara penuh, melainkan cukup menyebut nomor entri di constant pool. Desain ini membuat berkas ringkas dan referensi simbolik terkumpul di satu tempat untuk proses resolution saat class linking.
Mesin Berbasis Stack #
JVM adalah mesin virtual berbasis stack, bukan berbasis register seperti prosesor x86 atau ARM. Artinya, instruksi tidak menyebut register tujuan; nilai bekerja lewat operand stack milik tiap frame method. Operasi seperti penjumlahan berjalan tiga langkah: dorong operand pertama ke stack, dorong operand kedua, lalu instruksi iadd melepas keduanya dan menaruh hasilnya. Variabel lokal tersimpan di array local variable pada frame yang sama — instruksi iload dan istore memindahkan nilai antara array tersebut dan operand stack.
Model stack memberi dua keuntungan. Pertama, kompaktas: instruksi pendek seperti iload_1 cukup satu byte tanpa perlu menyandikan register. Kedua, portabilitas: karena tidak ada asumsi jumlah register prosesor, bytecode yang sama berjalan di mesin apa pun — JVM-lah yang menerjemahkan ke register fisik saat JIT mengompilasi kode panas.
Program Uji dan Bytecode-nya #
Mari kita kompilasi teori menjadi bukti. Program Kalkulator menghitung kuadrat sebuah bilangan lalu mencetaknya:
public class Kalkulator {
static int kuadrat(int x) {
return x * x;
}
public static void main(String[] args) {
String label = "hasil";
int nilai = 12;
int hasil = kuadrat(nilai);
System.out.println(label + " = " + hasil);
}
}Output yang Dihasilkan:
hasil = 144Program sederhana ini menyimpan seluruh konsep yang kita bahas: variabel lokal, pemanggilan method, field static milik pustaka, hingga penggabungan string. Alat javap dari folder bin JDK membongkar berkas hasil kompilasi menjadi teks yang dapat dibaca:
javac Kalkulator.java
javap -c KalkulatorCompiled from "Kalkulator.java"
public class Kalkulator {
public Kalkulator();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int kuadrat(int);
Code:
0: iload_0
1: iload_0
2: imul
3: ireturn
public static void main(java.lang.String[]);
Code:
0: ldc #7 // String hasil
2: astore_1
3: bipush 12
5: istore_2
6: iload_2
7: invokestatic #9 // Method kuadrat:(I)I
10: istore_3
11: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_1
15: iload_3
16: invokedynamic #21, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
21: invokevirtual #25 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: return
}Nomor di kolom kiri adalah offset — posisi instruksi di dalam method, sekaligus target lompatan. Nomor #7 atau #15 merujuk entri constant pool; angka pastinya bergantung versi javac, sehingga keluaran di komputer Anda boleh sedikit berbeda dari keluaran JDK 25 di atas.
Membaca Instruksi Satu per Satu #
Method kuadrat memperlihatkan pola stack paling murni. Empat baris berikut menelusuri eksekusinya untuk argumen 12:
0: iload_0dan1: iload_0: dua kali memuat nilai dari slot variabel lokal nomor 0 — parameterx— ke operand stack. Stack kini berisi[12, 12].2: imul: melepas dua nilaiintteratas stack, mengalikan keduanya (144), lalu menaruh hasilnya kembali. Stack menjadi[144].3: ireturn: melepas nilai teratas stack sebagai nilai balik method dan membongkar frame pemanggil.
Method main memperkenalkan instruksi lain yang sering kita jumpai sehari-hari:
0: ldc #7(load constant): menaruh konstanta"hasil"dari constant pool ke stack. Konstanta kecil seperti12memakai bentuk khusus —3: bipush 12menaruh bilangan byte langsung sebagai operand instruksi.2: astore_1dan5: istore_2: menyimpan referensi objek danintdari stack ke slot variabel lokal nomor 1 dan 2 — inilah eksekusi nyata deklarasilabeldannilai.7: invokestatic #9: memanggil method statickuadrat— frame baru dibuatkan, parameter dipindahkan ke local variable-nya, lalu hasil baliknya kembali ke stack pemanggil dan disimpan olehistore_3.11: getstatic #15: membaca field staticSystem.outdan menaruh referensinya ke stack — destinasi pencetakan berikutnya.16: invokedynamic #21: sejak Java 9, penggabungan string dikompilasi menjadi instruksi ini; JVM menyiapkan rutin penggabungan efisien (makeConcatWithConstants) saat pertama kali dieksekusi, bukan merangkaiStringBuildermanual di bytecode.21: invokevirtual #25: memanggil method virtualprintlnpada objekPrintStreamyang referensinya sudah menunggu di stack.
Verifikasi dan Keamanan Bytecode #
Bytecode tidak dieksekusi begitu saja. Saat linking — sebagaimana dibahas Chapter Class Loading — bytecode verifier memeriksa setiap method: stack operand tidak boleh meluap atau kehabisan isi, lompatan harus mendarat tepat di batas instruksi, dan tipe nilai yang ditaruh atau diambil harus konsisten. Instruksi seperti iload_0 di method kuadrat misalnya, hanya sah karena slot 0 memang bertipe int sesuai deskriptor (I)I. Pemeriksaan ini menjadikan berkas .class hasil rekayasa tidak sehat tertolak sebelum sempat dieksekusi, dan menjadikan model keamanan JVM kokoh sejak desain awal.
Bytecode kini bukan lagi kotak tertutup bagi kita. Namun instruksi hanya setengah cerita — instruksi bekerja di atas area memori yang menjadi panggungnya. Chapter berikutnya memetakan dua area terpenting tersebut: heap tempat objek hidup, dan stack tempat frame method bekerja.
Referensi #
- Oracle: The Java Virtual Machine Specification (Java SE 25) — Chapter 6 Instruction Set
- Oracle: javap Tool Reference (JDK 25)
- Oracle JDK 25 Documentation
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
Kalkulator.java
Unduh berkaspublic class Kalkulator {
static int kuadrat(int x) {
return x * x;
}
public static void main(String[] args) {
String label = "hasil";
int nilai = 12;
int hasil = kuadrat(nilai);
System.out.println(label + " = " + hasil);
}
}