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:
- 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.
- Preparation: JVM mengalokasikan memori untuk field
staticdan mengisinya nilai bawaan (0,false,null) — belum nilai dari kode kita. - Resolution: referensi simbolik seperti
java/lang/System.outdiubah 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:
| Loader | Sumber | Muat apa |
|---|---|---|
| Bootstrap | modul inti java.base dsb. | String, System, Object, seluruh java.lang |
| Platform | modul pendukung platform | java.sql, java.xml, JDK internal API |
| Application | classpath / modulepath milik program | class 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:
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:
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$PlatformClassLoaderMari kita telaah bagian pentingnya:
- Baris 4 (
tampil("String", String.class)): meminta loader dari classString. HasilnyanullkarenaStringtermuat oleh bootstrap loader — methodtampilmenerjemahkannullmenjadi teks"bootstrap"agar terbaca. - Baris 5 (
java.sql.DriverManager.class): class dari moduljava.sqldimuat oleh platform loader, terlihat dari nama classjdk.internal.loader.ClassLoaders$PlatformClassLoader. - Baris 7-10 (
getSystemClassLoader()dangetPlatformClassLoader()): method statisClassLoaderyang mengembalikan handle ke loader application (disebut juga system loader) dan platform. NamaAppClassLoadermenandaskan 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:
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);
}
}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:
--- program mulai, MuatClass belum diinisialisasi ---
setelah class literal : MuatClass
[MuatClass] blok static berjalan: class diinisialisasi
setelah Class.forName: MuatClass
objek Class sama : true
tertangkap: ClassNotFoundExceptionBaris-baris berikut menjelaskan urutan kejadian pada output:
- Baris 8 (
Class<?> literal = MuatClass.class;): class literal hanya menjalankan loading dan linking. Tidak ada pesan dari blokstaticpada tahap ini — bukti bahwa inisialisasi belum berjalan. - Baris 11 (
Class.forName("MuatClass")): pemanggilan satu argumen ini menjalankan ketiga fase sampai initialization. Baris[MuatClass] blok static berjalanmuncul tepat di sini, bukan sebelumnya. - Baris 14 (
Class.forNamekedua): JVM menemukan class sudah terinisialisasi, sehingga blokstatictidak dijalankan lagi — pesan blokstatichanya muncul satu kali pada keseluruhan output. PembandingandariForName == lagibernilaitruekarena kedua pemanggilan mengembalikan objekClassyang sama persis dari loader yang sama. - Baris 17-20 (
Class.forName("KelasYangTidakAda")): nama yang tidak ditemukan loader mana pun berujung padaClassNotFoundException— 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 #
- Oracle: The Java Virtual Machine Specification (Java SE 25) — Chapter 5 Loading, Linking, and Initializing
- Oracle: API ClassLoader (Java SE 25)
- Oracle: Learn Java — JVM
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
MuatClass.java
Unduh berkaspublic 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 berkaspublic 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 berkaspublic 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());
}
}
}