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:

java Kalkulator.java
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:

text
hasil = 144

Program 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:

bash Kompilasi lalu bedah bytecode
javac Kalkulator.java
javap -c Kalkulator
text javap -c Kalkulator (JDK 25, dipangkas)
Compiled 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:

  1. 0: iload_0 dan 1: iload_0: dua kali memuat nilai dari slot variabel lokal nomor 0 — parameter x — ke operand stack. Stack kini berisi [12, 12].
  2. 2: imul: melepas dua nilai int teratas stack, mengalikan keduanya (144), lalu menaruh hasilnya kembali. Stack menjadi [144].
  3. 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:

  1. 0: ldc #7 (load constant): menaruh konstanta "hasil" dari constant pool ke stack. Konstanta kecil seperti 12 memakai bentuk khusus — 3: bipush 12 menaruh bilangan byte langsung sebagai operand instruksi.
  2. 2: astore_1 dan 5: istore_2: menyimpan referensi objek dan int dari stack ke slot variabel lokal nomor 1 dan 2 — inilah eksekusi nyata deklarasi label dan nilai.
  3. 7: invokestatic #9: memanggil method static kuadrat — frame baru dibuatkan, parameter dipindahkan ke local variable-nya, lalu hasil baliknya kembali ke stack pemanggil dan disimpan oleh istore_3.
  4. 11: getstatic #15: membaca field static System.out dan menaruh referensinya ke stack — destinasi pencetakan berikutnya.
  5. 16: invokedynamic #21: sejak Java 9, penggabungan string dikompilasi menjadi instruksi ini; JVM menyiapkan rutin penggabungan efisien (makeConcatWithConstants) saat pertama kali dieksekusi, bukan merangkai StringBuilder manual di bytecode.
  6. 21: invokevirtual #25: memanggil method virtual println pada objek PrintStream yang 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

Kalkulator.java

Unduh berkas
java
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);
    }
}