Kalimat “objek disimpan di memori” terlalu kabur untuk JVM — kenyataannya ada dua area memori dengan karakter yang bertolak belakang. Heap adalah gudang besar bersama tempat semua objek dan array hidup, dikelola garbage collector, dan diakses lewat referensi. Stack adalah lorong sempit privat per thread tempat frame method menumpuk, bekerja cepat, dan dibongkar otomatis saat method selesai. Kebingungan antara keduanya menimbulkan salah paham klasik: mengira dua variabel bisa “menyalin objek”, atau mengira StackOverflowError berarti memori habis. Pada chapter ini kita pisahkan keduanya dengan tegas, lalu buktikan lewat program yang sengaja membuat objek berbagi referensi dan rekursi yang menumpuk frame tanpa dasar penghentian.
Dua Dunia yang Saling Melengkapi #
Setiap kali ekspresi new Pesanan(100) dieksekusi, JVM mengalokasikan memori untuk objek di heap — area besar yang dibagi seluruh thread. Variabel asli sendiri tidak berisi objek; variabel berisi referensi, alamat penunjuk ke objek di heap, dan referensi tersebut menempati slot variabel lokal pada stack frame milik method yang sedang berjalan. Maka penugasan Pesanan kedua = asli; tidak menggandakan objek — penugasan hanya menyalin penunjuk.
Stack tiap thread menyimpan frame untuk setiap pemanggilan method yang masih aktif. Satu frame memuat tiga isi: array local variables (parameter dan variabel method), operand stack tempat kerja instruksi bytecode sebagaimana dibahas Chapter Bytecode, dan data kendali seperti alamat balik ke pemanggil. Diagram berikut merangkum daur hidupnya:
Alur ini menjelaskan kecepatan stack. Alokasi frame hanya menggeser penunjuk ke puncak stack — tidak ada pencarian ruang bebas seperti di heap, tidak ada garbage collector yang ikut bekerja. Urutan pembongkaran pun pasti: frame yang paling akhir dibuat selalu yang pertama dilepas, mengikuti pola LIFO (last in, first out).
Satu Objek Dua Referensi #
Mari kita buktikan pemisahan heap dan stack dengan program. Class kecil Pesanan menyimpan satu field int; method main membuat satu objek, lalu mengarahkan dua variabel ke objek yang sama:
public class ReferensiDanObjek {
static class Pesanan {
int total;
Pesanan(int total) {
this.total = total;
}
}
public static void main(String[] args) {
Pesanan asli = new Pesanan(100); // satu objek lahir di heap
Pesanan kedua = asli; // hanya menyalin referensi
kedua.total = 250; // mengubah objek yang sama
System.out.println("asli.total = " + asli.total);
System.out.println("sama objek? = " + (asli == kedua));
kedua = null; // referensi kedua dilepas; objek tetap dijangkau 'asli'
System.out.println("setelah kedua = null, asli.total = " + asli.total);
}
}Output yang Dihasilkan:
asli.total = 250
sama objek? = true
setelah kedua = null, asli.total = 250Tiga baris berikut menafsirkan output tersebut:
- Baris 15 (
kedua.total = 250;): perubahan menembus objek di heap, bukan salinan. Ketikaasli.totaldibaca, nilainya sudah250— bukti keduanya menunjuk objek yang sama. - Baris 18 (
asli == kedua): operator==pada variabel referensi membandingkan penunjuk, bukan isi objek. Hasiltruemenegaskan satu objek dengan dua referensi. - Baris 21 (
kedua = null;): melepas satu referensi tidak memengaruhi objek selama masih ada penunjuk lain —aslitetap sah dipakai. Objek baru menjadi kandidat pemungutan garbage collector ketika referensi terakhir pun dilepas.
Ketika Frame Menumpuk Tanpa Batas #
Setiap pemanggilan method menambah satu frame di stack. Rekursi tanpa dasar penghentian menumpuk frame tanpa ujung hingga kapasitas stack habis — JVM pun melempar StackOverflowError. Program berikut sengaja melakukannya, lalu menangkap galatnya agar kita bisa melihat program tetap sehat:
public class RekursiTanpaBatas {
static void turun() {
turun(); // tidak ada dasar penghentian
}
public static void main(String[] args) {
try {
turun();
} catch (StackOverflowError e) {
System.out.println("tertangkap: " + e.getClass().getSimpleName());
}
System.out.println("program tetap berjalan sampai sini");
}
}Output yang Dihasilkan:
tertangkap: StackOverflowError
program tetap berjalan sampai siniDua hal penting dari demo ini. Pertama, StackOverflowError menyasar stack satu thread — heap tidak ikut penuh, dan thread lain tidak terganggu; karenanya main masih bisa mencetak baris terakhir setelah galat ditangkap. Kedua, kedalaman rekursi saat galat terjadi tidak tetap: bergantung ukuran frame, optimasi JIT, dan bendera -Xss. Pada mesin uji dengan stack bawaan, galat muncul pada sekitar sepuluh ribu panggilan; dengan -Xss8m JVM menampung hampir sepuluh kali lebih banyak. Konsekuensinya, rekursi dalam Java harus punya jaminan kedalaman terbatas — untuk struktur tak terduga, iterasi dengan ArrayDeque sebagai tumpukan manual lebih aman.
Mengatur Lebar Lorong dengan -Xss #
Ukuran stack per thread diatur bendera -Xss, misalnya java -Xss512k Aplikasi memberi tiap thread stack sebesar 512 KB. Nilai bawaannya bergantung sistem operasi dan arsitektur — umumnya 1 MB untuk thread platform di Linux 64-bit. Mengecilkan -Xss menghemat memori pada aplikasi ber-ribuan thread sekaligus menurunkan batas kedalaman rekursi; membesarkannya memberi ruang bagi rekursi dalam dengan biaya memori per thread yang lebih besar. Virtual thread — fitur Java 21 yang menjalankan jutaan tugas ringan — justru menyimpan frame-nya di heap sehingga tidak terikat batas -Xss; pembahasan lengkapnya ada pada Section concurrency.
Heap dan stack kini punya garis batas yang jelas di benak kita. Namun satu pertanyaan belum terjawab: di bagian heap mana objek baru lahir, dan bagaimana objek berpindah usia di dalamnya? Pertanyaan itu membawa kita ke Chapter Object Lifecycle di JVM — tetapi sebelum itu, ada satu area memori lain yang menyimpan bukan objek melainkan metadata class: Metaspace.
Referensi #
- Oracle: The Java Virtual Machine Specification (Java SE 25) — Chapter 2 Structure of the Java Virtual Machine
- Oracle: API StackOverflowError (Java SE 25)
- Oracle: java Tool Reference (JDK 25) — opsi -Xss
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ReferensiDanObjek.java
Unduh berkaspublic class ReferensiDanObjek {
static class Pesanan {
int total;
Pesanan(int total) {
this.total = total;
}
}
public static void main(String[] args) {
Pesanan asli = new Pesanan(100); // satu objek lahir di heap
Pesanan kedua = asli; // hanya menyalin referensi
kedua.total = 250; // mengubah objek yang sama
System.out.println("asli.total = " + asli.total);
System.out.println("sama objek? = " + (asli == kedua));
kedua = null; // referensi kedua dilepas; objek tetap dijangkau 'asli'
System.out.println("setelah kedua = null, asli.total = " + asli.total);
}
}
RekursiTanpaBatas.java
Unduh berkaspublic class RekursiTanpaBatas {
static void turun() {
turun(); // tidak ada dasar penghentian
}
public static void main(String[] args) {
try {
turun();
} catch (StackOverflowError e) {
System.out.println("tertangkap: " + e.getClass().getSimpleName());
}
System.out.println("program tetap berjalan sampai sini");
}
}