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.

text
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.

kotlin
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:

text
=== 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 15

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-5 (val angka, val nama, val data): angka disimpan sebagai primitif langsung di stack frame main. Sebaliknya, nama dan data menyimpan referensi; objek String "Budi" dan int[] {10, 20, 30} berada di heap.
  2. Baris 16 (panggilLapisPertama()): pemanggilan ini menambah satu stack frame baru di atas frame main. JVM menyimpan alamat kembali ke baris 17 dalam frame data.
  3. Baris 25 (Thread.currentThread().stackTrace): API Java ini mengembalikan array StackTraceElement yang merefleksikan isi stack thread saat ini. Elemen index 0 adalah frame paling dalam (innermost), yaitu panggilLapisKetiga sendiri.
  4. forEachIndexed { i, frame -> ... }: kita mencetak class, method, dan nomor baris setiap frame. Hasil yang terurut dari dalam ke luar memperlihatkan rantai pemanggilan main → 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 KotlinLokasiDikelola GCCatatan
Int, Long, Double (primitif)StackTidakHanya jika disimpan sebagai variabel lokal
Int?, Array<Int>HeapYaOtomatis boxing ke java.lang.Integer
IntArray, LongArrayHeapYa, array tunggalLebih efisien dari Array<Int>
String, data classHeapYaSama 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.Thread Java 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.