Stack vs Heap

Membedakan frame stack dan alokasi objek di heap serta melacak letak referensi.

Daftar isi

Program Java menyimpan datanya di dua area memori yang berbeda peran, yaitu stack untuk mencatat jalannya pemanggilan metode dan heap untuk menampung objek. Topik ini membedakan keduanya secara konkret: bagaimana frame stack lahir dan hilang bersama pemanggilan metode, tempat objek hasil new berada, serta bagaimana melacak letak sebuah referensi. Di akhir topik, perbedaan antara variabel lokal primitif dan referensi objek akan terlihat nyata, lengkap dengan akibatnya saat stack kehabisan ruang.

Dua area memori dengan peran berbeda#

Setiap thread di JVM memiliki stack sendiri, yaitu area memori yang mencatat urutan pemanggilan metode yang sedang berjalan. Setiap kali sebuah metode dipanggil, JVM menyiapkan satu frame, yakni catatan kerja yang berisi nilai parameter, variabel lokal, dan data bantu metode tersebut. Ketika metode selesai, frame-nya dibuang seluruhnya, sehingga isi variabel lokal tidak dapat diakses lagi dari luar. Berbeda dari stack, heap adalah satu area memori yang dibagi oleh semua thread untuk menampung objek. Objek yang dibuat dengan new selalu berakhir di heap. Pembersihannya tidak menunggu metode berakhir, melainkan ditangani garbage collection (GC), mekanisme JVM yang mengosongkan memori dari objek yang sudah tidak dirujuk.

Pemisahan ini menjelaskan dua sifat yang sering terasa saat menulis kode. Variabel lokal milik dua thread tidak pernah saling mengganggu karena masing-masing thread punya stack sendiri. Sebaliknya, objek yang dikirim antar-thread terlihat oleh semua pihak karena duduk di heap yang sama. Perbedaan letak tersebut juga menentukan masa hidup: variabel lokal hidup selebar lingkup metodenya, sedangkan objek hidup selama masih ada referensi yang menunjuknya.

Frame stack dan variabel lokal#

Frame adalah unit kerja terkecil di stack. Di dalamnya terdapat slot untuk setiap parameter dan variabel lokal, termasuk nilai primitif yang disimpan langsung di slot tersebut. Ukuran frame mengikuti jumlah slotnya, sehingga metode dengan banyak variabel lokal memakan stack lebih cepat daripada metode yang ramping. Karena setiap pemanggilan metode membuat frame baru, nilai primitif yang dikirim sebagai parameter disalin ke frame penerima, dan perubahan atas salinan itu tidak pernah terlihat oleh pemanggil. Perilaku ini dapat diamati lewat program berikut yang membandingkan parameter primitif dan parameter objek.

java UjiFrame.java
public class UjiFrame {

    static void proses(int angka, StringBuilder pesan) {
        angka = angka + 100;
        pesan.append(" diproses");
        System.out.printf("di dalam proses: angka=%d, pesan=%s%n", angka, pesan);
    }

    public static void main(String[] args) {
        int angka = 10;
        StringBuilder pesan = new StringBuilder("data");
        proses(angka, pesan);
        System.out.printf("di dalam main  : angka=%d, pesan=%s%n", angka, pesan);
    }
}
text Keluaran
di dalam proses: angka=110, pesan=data diproses
di dalam main  : angka=10, pesan=data diproses

Menjalankan programnya dari terminal cukup dengan satu perintah berikut.

bash
# berada di folder tempat UjiFrame.java disimpan
java UjiFrame.java
text Keluaran
di dalam proses: angka=110, pesan=data diproses
di dalam main  : angka=10, pesan=data diproses

Keluarannya memperlihatkan dua nasib yang berbeda. Parameter angka berupa primitif, sehingga metode proses menerima salinannya di frame sendiri, dan kenaikan angka = angka + 100 hanya mengubah salinan itu. Parameter pesan berupa referensi, sehingga pemanggilan append menyentuh objek StringBuilder yang sama di heap dan perubahannya terbaca dari main. Frame proses lalu dibuang saat metode berakhir, tetapi objek di heap tetap hidup karena masih dirujuk oleh variabel lokal di frame main.

Objek hidup di heap, referensi menunjuknya#

Operator new selalu mengalokasikan objek baru di heap, lalu mengembalikan referensinya. Referensi itu sendiri bisa berada di tiga tempat yang berbeda. Referensi dapat ditampung di variabel lokal pada sebuah frame, disimpan sebagai field di dalam objek lain yang juga berada di heap, atau diletakkan pada field static yang terikat pada kelasnya. Letak referensi menentukan berapa lama objek tetap hidup, karena objek hanya dikumpulkan GC setelah tidak ada jalur referensi yang menjangkaunya.

Diagram berikut memetakan contoh sebelumnya, yaitu dua frame di stack yang masing-masing menyimpan referensi menuju satu objek StringBuilder yang sama di heap.

Stack thread utamaframe mainangka = 10 (primitif)pesan (referensi)frame prosesangka = 110 (salinan)pesan (referensi)Heap (dibagi semua thread)StringBuilder"data diproses"
Dua frame di stack menunjuk objek yang sama di heap

Perhatikan bahwa yang berpindah-pindah antar-frame hanya referensinya, bukan objeknya. Objek StringBuilder hanya ada satu di heap, kendati dua frame sama-sama menunjuknya. Inilah alasan dua variabel yang diberi objek yang sama melihat perubahan yang sama, sekaligus alasan pembandingan == pada objek menguji kesamaan referensi, bukan isinya.

Saat stack habis: StackOverflowError#

Setiap frame memakan sebagian ruang stack, sehingga pemanggilan metode yang bertumpuk terlalu dalam menghabiskannya. Kasus yang paling sering adalah rekursi tanpa kasus dasar, yaitu metode yang memanggil dirinya tanpa kondisi berhenti. Ketika ruangnya habis, JVM melempar StackOverflowError. Program berikut sengaja memanggil metode tanpa akhir, lalu menangkap error tersebut untuk membuktikan letak masalahnya.

java UjiStackOverflow.java
public class UjiStackOverflow {

    static void turun() {
        turun();
    }

    public static void main(String[] args) {
        try {
            turun();
            System.out.println("baris ini tidak pernah tercetak");
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError tertangkap: stack habis oleh rekursi tanpa kasus dasar");
        }
        System.out.println("program tetap bisa melanjutkan eksekusi");
    }
}
text Keluaran
StackOverflowError tertangkap: stack habis oleh rekursi tanpa kasus dasar
program tetap bisa melanjutkan eksekusi

Jumlah panggilan yang termuat sebelum stack penuh tidak tetap, karena bergantung pada ukuran stack serta isi setiap frame. Pesan errornya pun khas, yaitu stack trace berisi ribuan baris nama metode yang sama berulang-ulang, penanda klasik rekursi tanpa kasus dasar. Error semacam ini bisa ditangkap seperti terlihat di atas, tetapi penangkapannya biasanya hanya menunda masalah, karena akar penyebabnya adalah rekursi yang tidak punya kondisi berhenti. Perbaiki kasus dasarnya, atau batasi kedalaman pemanggilan secara sadar.

Kesalahan umum#

  • Mengira field objek disimpan di stack. Hanya parameter dan variabel lokal yang berada di frame, sedangkan semua field tersimpan bersama objeknya di heap, termasuk field bertipe primitif.
  • Mengira new kadang dialokasikan di stack. Model bahasa Java menempatkan setiap objek di heap, dan pembacaan kode sebaiknya selalu mengikuti model ini.
  • Menaruh data besar di variabel lokal metode rekursif. Setiap frame membawa salinannya sendiri, sehingga konsumsi stack membesar berlipat sesuai kedalaman pemanggilan.
  • Menyamakan StackOverflowError dengan OutOfMemoryError. Yang pertama kehabisan ruang stack untuk frame, sedangkan yang kedua kehabisan heap untuk objek. Solusinya berbeda karena area yang habis juga berbeda.

Ringkasan#

  • Stack menyimpan frame pemanggilan metode milik masing-masing thread, sedangkan heap menyimpan semua objek dan dibagi oleh semua thread.
  • Variabel lokal primitif tinggal di frame dan mati bersama metodenya, sedangkan objek hidup di heap selama masih dirujuk.
  • Referensi dapat berada di frame, di field objek lain, atau di field static, dan letaknya menentukan umur objek.
  • Rekursi tanpa kondisi berhenti menghabiskan stack dan memunculkan StackOverflowError, bukan OutOfMemoryError.

Referensi#