Program Java kita tidak berdiri sendiri — class Aplikasi yang kita tulis bergantung pada String, System, Thread, dan ribuan class pustaka lain. JVM tidak memuat semuanya sekaligus saat startup; class dimuat secara malas, tepat pada saat pertama kali dibutuhkan. Proses dari berkas .class di disk menjadi class yang hidup dan siap dipakai melewati tiga fase besar — loading, linking, dan initialization — dengan aturan delegasi berjenjang yang menjaga integritas class inti. Pada chapter ini kita bedah ketiga fase tersebut, memetakan tiga lapis class loader bawaan, lalu membuktikan lewat program nyata bahwa inisialisasi class hanya terjadi satu kali.

Tiga Fase Hidup Sebuah Class #

Fase pertama, loading, adalah pembacaan berkas .class menjadi struktur internal JVM. Class loader mencari bytecode — dari disk, arsip JAR, atau jaringan — lalu membuat objek java.lang.Class sebagai representasi tunggal class tersebut di dalam JVM. Setiap class hanya punya satu objek Class per class loader, apa pun berkas asalnya.

Fase kedua, linking, menghubungkan class mentah tersebut dengan sumber daya JVM dan terbagi tiga langkah:

  1. Verification: bytecode verifier memeriksa instruksi — tipe konsisten, stack operand tidak meluap, lompatan mendarat di batas instruksi. Berkas hasil rekayasa tangan yang tidak sehat ditolak di sini.
  2. Preparation: JVM mengalokasikan memori untuk field static dan mengisinya nilai bawaan (0, false, null) — belum nilai dari kode kita.
  3. Resolution: referensi simbolik seperti java/lang/System.out diubah menjadi referensi langsung ke class, field, dan method yang sesungguhnya.

Fase ketiga, initialization, menjalankan ekspresi inisialisasi field static dan blok static dalam urutan penulisan. Baru setelah fase ini class siap dipakai penuh. JVM menjamin inisialisasi berjalan tepat satu kali per class — jaminan yang akan kita buktikan dengan program uji di bagian akhir chapter.

Mekanisme Delegasi Parent-First #

Setiap class loader punya loader induk, kecuali bootstrap loader yang berdiri paling atas. Ketika sebuah loader diminta memuat class, loader tersebut tidak langsung mencari sendiri — permintaan diteruskan lebih dulu ke induk. Hanya bila induk gagal menemukan class, loader anak mencari di sumbernya sendiri. Diagram berikut memperlihatkan alur delegasi tersebut:

Pola parent-first ini punya alasan keamanan. Bayangkan aplikasi berniat jahat menyertakan class palsu bernama java.lang.String — delegasi memastikan bootstrap loader selalu lebih dulu diminta, sehingga String asli dari modul java.base yang menang. Aplikasi tidak dapat menimpa class inti JVM dengan duplikat buatan sendiri.

Tiga Lapis Class Loader Bawaan #

Sejak Java 9 dan sistem modul (JPMS), hierarki loader bawaan berbentuk tiga lapis:

LoaderSumberMuat apa
Bootstrapmodul inti java.base dsb.String, System, Object, seluruh java.lang
Platformmodul pendukung platformjava.sql, java.xml, JDK internal API
Applicationclasspath / modulepath milik programclass yang kita tulis sendiri

Class loader bootstrap ditulis dalam bahasa native dan tidak punya representasi Java — String.class.getClassLoader() mengembalikan null. Program berikut memetakan siapa memuat siapa:

java PetaLoader.java
public class PetaLoader {
    public static void main(String[] args) {
        tampil("String", String.class);
        tampil("DriverManager", java.sql.DriverManager.class);

        System.out.println("system loader   : "
                + ClassLoader.getSystemClassLoader().getClass().getName());
        System.out.println("platform loader : "
                + ClassLoader.getPlatformClassLoader().getClass().getName());
    }

    static void tampil(String nama, Class<?> c) {
        ClassLoader loader = c.getClassLoader();
        String hasil = (loader == null) ? "bootstrap" : loader.getClass().getName();
        System.out.printf("%-13s dimuat oleh: %s%n", nama, hasil);
    }
}

Output yang Dihasilkan:

text
String        dimuat oleh: bootstrap
DriverManager dimuat oleh: jdk.internal.loader.ClassLoaders$PlatformClassLoader
system loader   : jdk.internal.loader.ClassLoaders$AppClassLoader
platform loader : jdk.internal.loader.ClassLoaders$PlatformClassLoader

Mari kita telaah bagian pentingnya:

  1. Baris 4 (tampil("String", String.class)): meminta loader dari class String. Hasilnya null karena String termuat oleh bootstrap loader — method tampil menerjemahkan null menjadi teks "bootstrap" agar terbaca.
  2. Baris 5 (java.sql.DriverManager.class): class dari modul java.sql dimuat oleh platform loader, terlihat dari nama class jdk.internal.loader.ClassLoaders$PlatformClassLoader.
  3. Baris 7-10 (getSystemClassLoader() dan getPlatformClassLoader()): method statis ClassLoader yang mengembalikan handle ke loader application (disebut juga system loader) dan platform. Nama AppClassLoader menandaskan class program kita yang dikompilasi akan dimuat di lapis ini.

Inisialisasi Hanya Terjadi Sekali #

Sekarang momen pembuktian. Kita siapkan class MuatClass dengan blok static yang mencetak pesan — blok ini hanya boleh berjalan satu kali sepanjang umur JVM. Lalu UjiMuat membandingkan dua cara memperoleh objek Class: class literal MuatClass.class yang hanya memuat tanpa inisialisasi, versus Class.forName yang memicu inisialisasi penuh:

java MuatClass.java
public class MuatClass {
    static int hitungInisialisasi = 0;

    static {
        hitungInisialisasi = 1;
        System.out.println("[MuatClass] blok static berjalan: class diinisialisasi");
    }

    static void info() {
        System.out.println("[MuatClass] hitungInisialisasi = " + hitungInisialisasi);
    }
}
java UjiMuat.java
public class UjiMuat {
    public static void main(String[] args) throws Exception {
        System.out.println("--- program mulai, MuatClass belum diinisialisasi ---");

        Class<?> literal = MuatClass.class; // memuat class, TANPA inisialisasi
        System.out.println("setelah class literal : " + literal.getName());

        Class<?> dariForName = Class.forName("MuatClass"); // memicu inisialisasi
        System.out.println("setelah Class.forName: " + dariForName.getName());

        Class<?> lagi = Class.forName("MuatClass"); // inisialisasi hanya sekali
        System.out.println("objek Class sama     : " + (dariForName == lagi));

        try {
            Class.forName("KelasNirwana");
        } catch (ClassNotFoundException e) {
            System.out.println("tertangkap: " + e.getClass().getSimpleName());
        }
    }
}

Output yang Dihasilkan:

text
--- program mulai, MuatClass belum diinisialisasi ---
setelah class literal : MuatClass
[MuatClass] blok static berjalan: class diinisialisasi
setelah Class.forName: MuatClass
objek Class sama     : true
tertangkap: ClassNotFoundException

Baris-baris berikut menjelaskan urutan kejadian pada output:

  1. Baris 8 (Class<?> literal = MuatClass.class;): class literal hanya menjalankan loading dan linking. Tidak ada pesan dari blok static pada tahap ini — bukti bahwa inisialisasi belum berjalan.
  2. Baris 11 (Class.forName("MuatClass")): pemanggilan satu argumen ini menjalankan ketiga fase sampai initialization. Baris [MuatClass] blok static berjalan muncul tepat di sini, bukan sebelumnya.
  3. Baris 14 (Class.forName kedua): JVM menemukan class sudah terinisialisasi, sehingga blok static tidak dijalankan lagi — pesan blok static hanya muncul satu kali pada keseluruhan output. Pembandingan dariForName == lagi bernilai true karena kedua pemanggilan mengembalikan objek Class yang sama persis dari loader yang sama.
  4. Baris 17-20 (Class.forName("KelasYangTidakAda")): nama yang tidak ditemukan loader mana pun berujung pada ClassNotFoundException — exception yang wajar ditangkap karena memuat class berdasar nama memang bisa gagal.

ClassNotFoundException vs NoClassDefFoundError #

Dua galat ini kerap tertukar karena sama-sama bicara soal class yang hilang. ClassNotFoundException adalah Exception yang dilempar oleh pemanggilan eksplisit seperti Class.forName ketika nama class tidak ditemukan — kondisi yang diantisipasi program, layak ditangkap blok try-catch. Sebaliknya, NoClassDefFoundError adalah Error yang muncul saat JVM gagal menemukan class yang seharusnya sudah ada — misalnya program dikompilasi dengan class pendamping di classpath, tetapi saat berjalan berkasnya hilang dari classpath. NoClassDefFoundError biasanya menandakan masalah pengemasan aplikasi, bukan alur program normal.

Setelah class hidup di dalam JVM, eksekusi bytecode tidak lagi mengurus nama berkas — mesin bekerja pada tumpukan instruksi yang dikenal sebagai bytecode. Chapter berikutnya mengajak kita membaca instruksi tersebut langsung lewat alat javap.

Referensi #

Berkas Sumber

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

MuatClass.java

Unduh berkas
java
public class MuatClass {
    static int hitungInisialisasi = 0;

    static {
        hitungInisialisasi = 1;
        System.out.println("[MuatClass] blok static berjalan: class diinisialisasi");
    }

    static void info() {
        System.out.println("[MuatClass] hitungInisialisasi = " + hitungInisialisasi);
    }
}

PetaLoader.java

Unduh berkas
java
public class PetaLoader {
    public static void main(String[] args) {
        tampil("String", String.class);
        tampil("DriverManager", java.sql.DriverManager.class);

        System.out.println("system loader   : "
                + ClassLoader.getSystemClassLoader().getClass().getName());
        System.out.println("platform loader : "
                + ClassLoader.getPlatformClassLoader().getClass().getName());
    }

    static void tampil(String nama, Class<?> c) {
        ClassLoader loader = c.getClassLoader();
        String hasil = (loader == null) ? "bootstrap" : loader.getClass().getName();
        System.out.printf("%-13s dimuat oleh: %s%n", nama, hasil);
    }
}

UjiMuat.java

Unduh berkas
java
public class UjiMuat {
    public static void main(String[] args) throws Exception {
        System.out.println("--- program mulai, MuatClass belum diinisialisasi ---");

        Class<?> literal = MuatClass.class; // memuat class, TANPA inisialisasi
        System.out.println("setelah class literal : " + literal.getName());

        Class<?> dariForName = Class.forName("MuatClass"); // memicu inisialisasi
        System.out.println("setelah Class.forName: " + dariForName.getName());

        Class<?> lagi = Class.forName("MuatClass"); // inisialisasi hanya sekali
        System.out.println("objek Class sama     : " + (dariForName == lagi));

        try {
            Class.forName("KelasNirwana");
        } catch (ClassNotFoundException e) {
            System.out.println("tertangkap: " + e.getClass().getSimpleName());
        }
    }
}