Saat program Kotlin berjalan di atas JVM, memori dibagi menjadi dua wilayah utama: stack dan heap. Pembagian ini bukan sekadar detail implementasi; struktur tersebut menentukan kapan sebuah nilai bebas dari memori, kapan terjadi stack overflow, dan di mana garbage collector akan bekerja. Pada chapter ini kita akan membedah kedua wilayah memori tersebut, memahami konsep stack frame per pemanggilan method, serta mendemonstrasikannya dengan Thread.currentThread().stackTrace.
Stack: Tempat Primitif dan Referensi #
Stack adalah struktur memori LIFO (Last-In First-Out) yang dikelola per thread. Setiap kali sebuah thread memanggil method, JVM mengalokasikan satu stack frame di atas thread stack. Stack frame berisi:
- Local variable array: primitif dan referensi lokal (misal
val angka = 42). - Operand stack: ruang kerja sementara untuk eksekusi instruksi bytecode.
- Frame data: referensi ke constant pool dan pointer untuk mengembalikan kontrol ke pemanggil.
Ukuran stack per thread default biasanya 512 KB hingga 1 MB. Method yang rekursif sangat dalam dapat memenuhi stack dan memicu StackOverflowError.
Thread Stack | Heap (shared antar thread)
------------------------------|-----------------------------
[main frame] | String "Budi"
- local: angka = 42 | int[] {10, 20, 30}
- local: nama → ────────────┼──→
- local: data → ────────────┼──→
[panggilLapisPertama frame] | (objek lain yang masih hidup)
[panggilLapisKedua frame] |
[panggilLapisKetiga frame] | (objek yang tidak lagi direferensikan
| akan diambil alih oleh GC)Heap: Tempat Objek #
Heap adalah area memori bersama yang dikelola oleh garbage collector. Semua objek — instance class, array, instance String — dialokasikan di heap. Variabel lokal di stack hanya menyimpan referensi ke objek di heap, bukan objeknya sendiri. Saat tidak ada lagi referensi aktif yang menunjuk ke sebuah objek, objek tersebut menjadi kandidat pengumpulan oleh GC.
Pembagian tanggung jawab ini berdampak langsung pada cara kita menulis kode: primitif (Int, Long, Double) yang disimpan sebagai variabel lokal bebas dari GC, sedangkan objek seperti List<String> atau data class mengalokasikan ruang di heap dan menambah beban GC.
Demonstrasi Stack Frame #
Mari kita lihat secara langsung bagaimana stack frame dibangun dan dihancurkan saat pemanggilan method berantai.
fun main() {
// Variabel primitif dan reference di stack frame
val angka = 42 // primitif Int di stack frame
val nama = "Budi" // reference String ke objek di heap
val data = intArrayOf(10, 20, 30) // reference array ke objek di heap
println("=== Memori: Stack dan Heap ===")
println("angka (primitif) = $angka")
println("nama (reference) = $nama dengan panjang ${nama.length}")
println("data (reference array) = ${data.toList()} ukuran ${data.size}")
// Mendemonstrasikan stack frame setiap pemanggilan method
println()
println("=== Stack Trace ===")
panggilLapisPertama()
}
fun panggilLapisPertama() {
panggilLapisKedua()
}
fun panggilLapisKedua() {
panggilLapisKetiga()
}
fun panggilLapisKetiga() {
// Thread.currentThread().stackTrace menampilkan frame saat ini
val jejak = Thread.currentThread().stackTrace
println("Jumlah frame: ${jejak.size}")
println("Frame paling atas (innermost):")
jejak.take(5).forEachIndexed { i, frame ->
println(" [$i] ${frame.className}.${frame.methodName}() baris ${frame.lineNumber}")
}
}Output yang Dihasilkan:
=== Memori: Stack dan Heap ===
angka (primitif) = 42
nama (reference) = Budi dengan panjang 4
data (reference array) = [10, 20, 30] ukuran 3
=== Stack Trace ===
Jumlah frame: 6
Frame paling atas (innermost):
[0] java.lang.Thread.getStackTrace() baris 2451
[1] Heap_stackKt.panggilLapisKetiga() baris 28
[2] Heap_stackKt.panggilLapisKedua() baris 23
[3] Heap_stackKt.panggilLapisPertama() baris 19
[4] Heap_stackKt.main() baris 15Mari kita telaah baris-baris kode di atas:
- Baris 3-5 (
val angka,val nama,val data):angkadisimpan sebagai primitif langsung di stack framemain. Sebaliknya,namadandatamenyimpan referensi; objekString "Budi"danint[] {10, 20, 30}berada di heap. - Baris 16 (
panggilLapisPertama()): pemanggilan ini menambah satu stack frame baru di atas framemain. JVM menyimpan alamat kembali ke baris 17 dalam frame data. - Baris 25 (
Thread.currentThread().stackTrace): API Java ini mengembalikan arrayStackTraceElementyang merefleksikan isi stack thread saat ini. Elemen index0adalah frame paling dalam (innermost), yaitupanggilLapisKetigasendiri. forEachIndexed { i, frame -> ... }: kita mencetak class, method, dan nomor baris setiap frame. Hasil yang terurut dari dalam ke luar memperlihatkan rantai pemanggilanmain → panggilLapisPertama → panggilLapisKedua → panggilLapisKetiga → getStackTrace.
Primitif di Stack vs Objek di Heap #
Karena primitif disimpan langsung di stack, mereka tidak ikut membebani GC. Sebaliknya, tipe wrapper seperti java.lang.Integer adalah objek di heap. Tabel berikut merangkum perbedaan tersebut:
| Tipe Kotlin | Lokasi | Dikelola GC | Catatan |
|---|---|---|---|
Int, Long, Double (primitif) | Stack | Tidak | Hanya jika disimpan sebagai variabel lokal |
Int?, Array<Int> | Heap | Ya | Otomatis boxing ke java.lang.Integer |
IntArray, LongArray | Heap | Ya, array tunggal | Lebih efisien dari Array<Int> |
String, data class | Heap | Ya | Sama seperti objek biasa |
Stack Overflow #
Stack memiliki ukuran terbatas. Rekursi tanpa base case atau kedalaman rekursif yang melebihi batas thread stack memicu StackOverflowError. Kita dapat memperbesar ukuran stack per thread dengan flag -Xss, namun cara ini jarang menjadi solusi yang tepat; algoritma iteratif biasanya lebih aman.
Referensi Lanjutan #
- JVM Guide — panduan resmi HotSpot JVM Java SE 21 untuk struktur heap dan stack runtime
- Java SE 21 Thread
— dokumentasi
java.lang.ThreadJava SE 21 untuk stack frame tiap thread
Penutup #
Pemisahan stack dan heap adalah pondasi dari model memori JVM. Setelah memahami di mana nilai disimpan, kita siap membahas bagaimana objek di heap yang sudah tidak terpakai dikembalikan ke ruang memori — materi pada chapter Garbage Collection.