Saat kita menekan Enter pada perintah java PerjalananKode, berlanjutlah sebuah rangkaian kejadian yang panjang: berkas sumber dibaca, diterjemahkan, diperiksa, lalu dieksekusi lapis demi lapis. Memahami rangkaian tersebut adalah bekal utama untuk membaca pesan error compiler, memahami performa aplikasi, dan mengerti mengapa program Java bisa berjalan di sistem operasi mana pun. Chapter ini mengikuti satu program kecil dari berkas .java sampai output tercetak di layar — melewati compiler javac, berkas .class, class loader, verifikasi bytecode, sampai eksekusi oleh interpreter dan JIT compiler.

Peta Besar Perjalanan Kode #

Alur kerja Java terdiri atas dua fase besar. Fase kompilasi menerjemahkan kode sumber menjadi bytecode; fase eksekusi memuat bytecode tersebut ke JVM lalu menjalankannya. Diagram berikut merangkum seluruh perjalanan:

Kita akan menyusuri tiap tahap diagram di atas secara berurutan, lalu membuktikannya dengan program sungguhan dan membongkar bytecode hasil kompilasi.

Fase Kompilasi dari Berkas .java ke Berkas .class #

Compiler javac membaca berkas sumber dan menghasilkan berkas .class — bukan instruksi mesin x86 atau ARM, melainkan bytecode: instruksi untuk mesin virtual. Setiap berkas .class berisi satu class lengkap dengan struktur baku: nomor magic 0xCAFEBABE sebagai tanda pengenal format, nomor versi classfile (major version 69 untuk Java 25), constant pool berisi literal dan referensi, daftar field, daftar method, beserta atributnya. Format baku inilah yang membuat JVM dari vendor mana pun dapat menjalankan berkas .class hasil kompilasi javac mana pun — kompatibilitas write once, run anywhere bekerja pada level berkas ini.

Pemeriksaan tipe juga terjadi pada fase ini. javac menolak kode yang menimpahkan String ke variabel int, memanggil method yang tidak ada, atau lupa menginisialisasi variabel lokal — semuanya dilaporkan sebagai compile-time error sebelum program sempat berjalan. Inilah keunggulan utama bahasa bertipe statis: sekumpulan kesalahan tertutup sejak awal.

Demonstrasi Kompilasi dan Eksekusi #

Mari kita buktikan seluruh teori dengan satu program. PerjalananKode mencetak identitas JVM yang menjalankannya, lalu menghitung total belanja buku milik Budi:

public class PerjalananKode {

    public static void main(String[] args) {
        System.out.println("Berjalan di " + System.getProperty("java.vm.name"));
        System.out.println("Versi runtime: " + System.getProperty("java.version"));

        int hargaBuku = 45000;
        int jumlah = 3;
        int total = hargaBuku * jumlah;

        System.out.println("Budi membeli " + jumlah + " buku, total Rp" + total);
    }
}

Output yang Dihasilkan:

text
Berjalan di OpenJDK 64-Bit Server VM
Versi runtime: 25.0.4.1
Budi membeli 3 buku, total Rp135000

Dua langkah baku untuk menjalankan program di atas:

bash
javac PerjalananKode.java   # menghasilkan PerjalananKode.class
java PerjalananKode          # JVM memuat class lalu mengeksekusi bytecode

Sejak Java 11 ada juga pintasan satu langkah: java PerjalananKode.java mengompilasi berkas ke memori lalu langsung menjalankannya tanpa meninggalkan berkas .class di folder.

Mari kita telaah isi program dan hubungannya dengan alur kompilasi-eksekusi:

  1. Baris 4-5 (System.getProperty("java.vm.name") dan getProperty("java.version")): membaca property runtime saat program berjalan. Nilainya datang dari JVM yang sedang mengeksekusi bytecode, sehingga dua baris ini menjadi bukti konkret mesin mana yang bekerja di balik layar.
  2. Baris 7-9 (int hargaBuku = 45000; dst.): tiga variabel bertipe int. Compiler javac mencatat tipe ketat tersebut ke dalam bytecode — hasil perkalian hargaBuku * jumlah dijamin bertipe int sejak kompilasi.
  3. Baris 11 ("Budi membeli " + jumlah + ...): penggabungan teks dan angka. javac modern tidak menyusun rantai StringBuilder manual, melainkan menandai ekspresi ini agar JVM menyiapkan rutin penggabungan yang efisien saat program berjalan.
  4. Baris 3 (public static void main(String[] args)): titik masuk yang dicari peluncur JVM — tanpa method berbentuk persis seperti ini, peluncur menolak menjalankan class.

Melihat Isi Bytecode dengan javap #

Berkas .class bisa dibongkar isinya memakai alat javap dari folder bin JDK. Perintah javap -c PerjalananKode menampilkan instruksi bytecode milik tiap method; berikut potongan method main hasil pemangkasan:

text javap -c PerjalananKode (dipangkas)
public static void main(java.lang.String[]);
  Code:
     0: getstatic     #7      // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #13     // String java.vm.name
     5: invokestatic  #15     // Method java/lang/System.getProperty:(Ljava/lang/String;)Ljava/lang/String;
     ...
    32: ldc           #31     // int 45000
    34: istore_1
    35: iconst_3
    36: istore_2
    37: iload_1
    38: iload_2
    39: imul
    40: istore_3
    ...
    54: return

Setiap instruksi diberi nomor offset sebagai posisinya di dalam method. Potongan di atas memperlihatkan inti perhitungan belanja Budi: ldc menaruh konstanta 45000 ke stack operand, istore_1 menyimpannya ke slot variabel lokal pertama, iload mengambil nilai kembali ke stack, imul mengalikan dua nilai bertipe int, lalu istore_3 menyimpan hasilnya. Tidak ada instruksi “perkalian variabel” — bytecode bekerja lewat stack operand, persis seperti spesifikasi JVM mendefinisikannya.

Class Loader dan Verifikasi Bytecode #

Saat peluncur java berjalan, JVM tidak langsung mengeksekusi main. Class loader membaca berkas .class, membuat representasi class di memori, menautkannya, baru menginisialisasi. Ada tiga lapis class loader bawaan: bootstrap memuat modul inti seperti java.base, platform memuat modul pendukung seperti java.sql, dan application memuat class milik program kita dari classpath. Proses pemuatan ini berjenjang melalui mekanisme delegasi: permintaan class selalu diteruskan ke loader induk lebih dahulu agar class inti tidak tertimpa duplikat buatan aplikasi.

Sebelum dieksekusi, bytecode melewati bytecode verifier pada tahap penautan. Verifier memeriksa bahwa instruksi konsisten dengan tipe, stack operand tidak meluap atau kehabisan isi, lompatan tidak mendarat di tengah instruksi, dan field diakses dengan tipe yang benar. Pemeriksaan ini membuat berkas .class hasil rekayasa tangan yang tidak sehat tertolak lebih awal — keamanan eksekusi kode tak dipercaya adalah tujuan sejak Java pertama dirancang.

Eksekusi dengan Interpreter dan JIT Compiler #

Bytecode lalu dieksekusi dua cara yang bekerja bergandengan. Interpreter menerjemahkan instruksi demi instruksi sejak awal — startup cepat, tetapi kecepatan eksekusinya moderat. Sementara itu, Just-In-Time (JIT) compiler HotSpot mengamati method mana yang sering dieksekusi (disebut kode panas), lalu mengompilasi method tersebut menjadi instruksi mesin asli prosesor. Eksekusi berikutnya memakai hasil kompilasi tersebut — inilah sebabnya program Java berat dihitung mencapai performa tinggi setelah berjalan beberapa saat. JIT bekerja bertingkat: compiler cepat menyusun kode mesin lebih dulu, compiler pengoptimal menyusul untuk kode yang terbukti sangat sering dipakai.

Sekarang rangkaian lengkapnya sudah kita susuri: javac menghasilkan bytecode, class loader memuatnya, verifier memeriksanya, interpreter dan JIT mengeksekusinya. Pemahaman ini menjadi dasar saat kita membandingkan Java dengan bahasa lain yang memilih jalur berbeda — Python yang dinamis, JavaScript yang berbasis event loop, Go yang dikompilasi native. Chapter berikutnya menghadirkan perbandingan tersebut satu per satu.

Referensi #

Berkas Sumber

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

PerjalananKode.java

Unduh berkas
java
public class PerjalananKode {

    public static void main(String[] args) {
        System.out.println("Berjalan di " + System.getProperty("java.vm.name"));
        System.out.println("Versi runtime: " + System.getProperty("java.version"));

        int hargaBuku = 45000;
        int jumlah = 3;
        int total = hargaBuku * jumlah;

        System.out.println("Budi membeli " + jumlah + " buku, total Rp" + total);
    }
}