Stack Overflow dalam pengembangan mobile — apa itu, penyebab dan metode pencegahan

Penulis: IT Sectr Diterbitkan: 2026-03-29 Waktu membaca: 9 mnt

Stack Overflow — kesalahan luberan tumpukan panggilan (java.lang.StackOverflowError), yang terjadi ketika kedalaman maksimum tumpukan thread terlampaui. Menurut Java Virtual Machine Specification, kedalaman tipikal tumpukan di JVM adalah 1024 frame untuk sistem 64-bit. Penyebab utama — rekursi tak terbatas tanpa kondisi dasar berhenti.

Poin utama

  • StackOverflowError — kesalahan JVM saat melampaui batas kedalaman tumpukan panggilan
  • Kedalaman tumpukan terbatas dan berkisar 512–2048 frame tergantung konfigurasi
  • Rekursi tak terbatas — penyebab paling umum StackOverflowError
  • Rekursi ekor tidak dioptimalkan di JVM, tidak seperti bahasa fungsional
  • Penggantian iteratif rekursi — cara andal untuk mencegah luberan

Apa itu Stack Overflow

StackOverflowError — adalah kesalahan fatal dari Java Virtual Machine (JVM) atau Android Runtime (ART), yang terjadi ketika tumpukan panggilan thread mencapai kedalaman maksimum yang diizinkan. Berbeda dengan OutOfMemoryError (kekurangan Heap), StackOverflowError terkait dengan area memori lain — tumpukan, tempat frame panggilan metode dan variabel lokal disimpan.

Setiap panggilan metode membuat frame di tumpukan: alamat kembali, parameter dan variabel lokal. Saat kembali dari metode, frame dihancurkan. Jika metode memanggil dirinya sendiri (rekursi) tanpa kondisi dasar, frame menumpuk hingga tumpukan penuh. JVM tidak dapat mengalokasikan frame baru dan melempar StackOverflowError dengan pesan „null” (di Java) atau dengan indikasi string tumpukan yang berulang tak terbatas.

Ukuran tumpukan thread ditetapkan saat pembuatan dan tidak berubah selama eksekusi. Di Android, ukuran tipikal tumpukan thread utama adalah 32–48 KB, yang memberikan kedalaman sekitar 512–1024 frame untuk metode tanpa banyak variabel lokal. Untuk thread latar belakang, ukuran default lebih kecil — 16–24 KB.

Bagaimana cara kerja tumpukan panggilan

Tumpukan panggilan (Call Stack) — adalah struktur data LIFO (Last In, First Out) yang mengatur urutan eksekusi metode. Setiap kali program memanggil metode, JVM membuat frame di tumpukan dan menempatkannya di atas. Saat metode selesai, frame dihapus.

Setiap frame berisi: operand stack (tumpukan operan untuk instruksi bytecode), array of local variables (termasuk this), reference to constant pool dan alamat kembali. Semakin banyak variabel lokal yang dimiliki metode, semakin besar ukuran frame-nya dan semakin sedikit metode yang dapat dipanggil sebelum tumpukan penuh. Metode dengan 10 parameter dan 20 variabel lokal memakan ruang sekitar 3 kali lebih banyak daripada metode tanpa parameter.

Di Android ART menggunakan implementasi tumpukannya sendiri, berbeda dari Desktop JVM. ART dapat meningkatkan tumpukan secara dinamis dalam batas tertentu, tetapi untuk setiap thread tetap ada batas keras. Thread utama (thread UI) memiliki tumpukan terbesar karena seluruh siklus hidup Activity dan pemrosesan event dijalankan di atasnya.

kotlin
// Rekursi yang menyebabkan StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // tidak ada kondisi dasar
}

// Panggilan akan menyebabkan StackOverflowError pada kedalaman ~1000
recursiveCall(0)

Penyebab utama luberan tumpukan

Lima skenario tipikal menyebabkan StackOverflowError di aplikasi mobile. Sebagian besar terkait dengan rekursi, tetapi ada juga penyebab yang kurang jelas.

Rekursi tak terbatas tanpa kondisi dasar

Penyebab paling umum. Pengembang menulis metode rekursif tanpa kondisi berhenti atau dengan kondisi yang tidak pernah menjadi true. Setiap panggilan menambahkan frame, dan tumpukan penuh setelah 500–2000 iterasi tergantung ukuran frame. Contoh tipikal: perhitungan faktorial n! tanpa memeriksa n == 0.

Periksa kondisi dasar di awal setiap metode rekursif. Di Kotlin, gunakan require() atau check() untuk validasi parameter di awal. Untuk rekursi dalam (lebih dari 100 level) pertimbangkan penggantian dengan pendekatan iteratif.

Ketergantungan siklik dalam konstruktor

Kelas A membuat instance B, kelas B membuat instance A — ini adalah ketergantungan siklik dalam konstruktor. Saat mencoba membuat A, konstruktor B dipanggil, yang memanggil konstruktor A, dan seterusnya hingga StackOverflowError. Framework DI (Dagger, Hilt) mendeteksi siklus seperti itu pada tahap kompilasi, tetapi pembuatan objek manual tidak menangkapnya.

Gunakan Dependency Injection dengan graf ketergantungan: Dagger atau Koin memeriksa siklus pada tahap build. Jika siklus tidak terhindarkan, ganti ketergantungan langsung dengan antarmuka dengan inisialisasi malas atau pabrik Provider.

kotlin
// Ketergantungan siklik — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Solusi malas
class A(private val bProvider: Provider<B>)

Rekursi dalam saat melintasi graf

Melintasi pohon View (ViewGroup.getChildAt()), sistem file atau struktur JSON melalui rekursi dapat melampaui batas tumpukan pada kedalaman lebih dari 500–1000 elemen. Android ViewGroup dengan sarang 20 level jarang terjadi, tetapi penguraian rekursif JSON dengan 2000 objek bersarang adalah skenario nyata.

Ganti pelintasan rekursif dengan iteratif melalui Stack<T> eksplisit atau ArrayDeque. Ini sepenuhnya menghilangkan risiko luberan tumpukan karena objek di heap tidak dibatasi oleh batas tumpukan. BFS (Breadth-First Search) melalui Queue juga memecahkan masalah.

Pemrosesan onConfigurationChanged yang salah

Penyebab spesifik Android: panggilan siklik metode siklus hidup saat pemrosesan konfigurasi yang salah. Misalnya, di onConfigurationChanged, recreate() dipanggil, yang kembali memanggil onConfigurationChanged, dan seterusnya hingga StackOverflowError. Serupa: setContentView() di dalam onLayout(), yang menyebabkan pengukuran dan layout berulang.

Jangan panggil recreate() di dalam metode yang terkait dengan perubahan konfigurasi. Untuk pembaruan UI saat perubahan tema, gunakan setTheme() tanpa recreate. Untuk perubahan orientasi dinamis — requestOrientation() sekali, tanpa flag di konfigurasi.

Serialisasi dengan referensi siklik

Gson, Moshi atau Kotlin Serialization saat mencoba menserialisasi objek dengan referensi siklik (A merujuk ke B, B merujuk ke A) masuk ke rekursi tak terbatas dan crash dengan StackOverflowError. Ini adalah masalah umum saat menserialisasi Entity dengan bidirectional Relationship (JPA, Room dengan ForeignKey).

Gunakan @Transient, @JsonIgnore atau @kotlinx.serialization.Transient untuk salah satu sisi siklus. Untuk Gson — JsonSerializer dengan batasan kedalaman eksplisit. Untuk Room — jangan pernah menserialisasi Entity secara langsung, gunakan mapper DTO.

Cara mendiagnosis dan memperbaiki StackOverflowError

Diagnosis StackOverflowError lebih sederhana daripada kesalahan memori lainnya: stack trace dalam大多数 kasus menunjukkan urutan panggilan yang berulang. Ini segera menunjukkan rekursi.

Membaca stack trace

Stack trace StackOverflowError unik: setelah 200–500 baris pertama, pengulangan pola panggilan yang sama dimulai. JVM memotong baris berulang di akhir dan menampilkan „... 1234 more”. Jumlah baris tidak berulang sebelum „...” menunjukkan kedalaman rekursi yang menyebabkan kesalahan.

Baca baris pertama stack trace — baris tersebut menunjukkan dari metode mana pengulangan dimulai. Temukan metode yang memanggil dirinya sendiri atau membuat rantai panggilan yang kembali ke metode tersebut. Perbaiki kondisi dasar atau ganti rekursi dengan loop.

Meningkatkan ukuran tumpukan (solusi sementara)

Sementara masalah dapat diatasi dengan meningkatkan ukuran tumpukan melalui flag JVM -Xss. Di Android, ukuran tumpukan diatur melalui AndroidManifest: android:largeHeap tidak memengaruhi tumpukan. Untuk meningkatkan tumpukan thread dalam kode: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — ukuran yang diinginkan dalam byte.

kotlin
// Membuat thread dengan tumpukan yang diperbesar
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

Penting: meningkatkan tumpukan tidak menyelesaikan masalah, hanya menundanya. Dengan rekursi 10.000 level, tumpukan 64 KB akan diganti dengan tumpukan 128 KB, yang memberikan 20.000 level — tetapi kesalahan tetap akan terjadi, hanya lebih lambat. Satu-satunya solusi yang benar — penggantian iteratif rekursi.

Mengganti rekursi dengan iterasi

Algoritma iteratif tidak menggunakan tumpukan panggilan untuk menyimpan keadaan antara — mereka menyimpannya di heap (Stack<T> atau ArrayDeque). Pelintasan pohon biner, perhitungan faktorial, Fibonacci — rekursi apa pun dapat diubah menjadi iterasi melalui tumpukan eksplisit.

kotlin
// Pelintasan pohon iteratif — tanpa risiko StackOverflow
fun traverseIterative(root: Node?) {
    val stack = ArrayDeque<Node>()
    stack.push(root)
    while (stack.isNotEmpty()) {
        val node = stack.pop() ?: continue
        process(node)
        node.right?.let { stack.push(it) }
        node.left?.let { stack.push(it) }
    }
}

Cara mencegah Stack Overflow

Pencegahan StackOverflowError adalah seperangkat aturan dan alat yang mendeteksi potensi siklus rekursif sebelum masuk ke produksi.

Batas kedalaman rekursi di build debug

Tambahkan penghitung kedalaman pelindung di metode rekursif di build debug. Jika kedalaman melebihi ambang (misalnya 1000), lemparkan pengecualian dengan pesan yang jelas. Ini mengubah StackOverflowError dengan trace yang tidak terbaca menjadi pengecualian bisnis yang dapat dipahami.

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("Rekursi melebihi 1000 level")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

Analisis statis kode

Detekt (Kotlin) dan Infer (Facebook) menemukan potensi rekursi tak terbatas di tingkat analisis statis. Detekt memiliki aturan PotentiallyInfiniteRecursion yang memperingatkan tentang self-call tanpa perubahan parameter. Sertakan dalam set aturan CI dan atur severity ke error.

Code Review dengan fokus pada rekursi

Pada code review perhatikan: metode self-call apa pun, panggilan rekursif di dalam lambda (fungsi inline Kotlin), panggilan siklik antara kelas yang berbeda, rekursi di property delegates. Untuk setiap metode rekursif periksa: apakah ada kondisi dasar, apakah parameter berubah di setiap langkah, apakah perubahan parameter menjamin tercapainya kondisi dasar.

Transformasi rekursi ekor (terbatas)

Kotlin mendukung pengubah tailrec: jika metode rekursif ditandai dengan tailrec dan panggilan adalah panggilan ekor (operasi terakhir), kompiler mengubahnya menjadi iterasi. Namun tailrec hanya berfungsi untuk self-call (metode langsung memanggil dirinya sendiri), tidak berfungsi untuk rekursi timbal balik dan tidak didukung di versi Kotlin yang kompatibel dengan Android sebelum 1.5.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // panggilan ekor
}

Pertanyaan yang sering diajukan

Bisakah StackOverflowError ditangkap melalui try-catch?

Bisa, tetapi hanya di tingkat Java. Error, seperti Exception, adalah Throwable. Namun setelah StackOverflowError, tumpukan rusak — frame yang tidak muat tidak dapat diselesaikan dengan benar. Upaya membuat objek baru di blok catch dapat menyebabkan StackOverflowError lagi.

Berapa ukuran tumpukan default di Android?

Untuk thread utama — 32–48 KB, untuk thread latar — 16–24 KB. Ukuran pasti tergantung pada versi Android dan pabrikan perangkat. ART menggunakan perluasan tumpukan dinamis, tetapi tidak lebih dari 2× dari nilai awal.

Bisakah rekursi ekor mencegah StackOverflowError?

Di Kotlin — ya, jika metode ditandai dengan tailrec. Kompiler mengubah rekursi ekor menjadi iterasi, sepenuhnya menghilangkan pertumbuhan tumpukan. Di Java, rekursi ekor tidak dioptimalkan oleh JVM (tidak seperti bahasa fungsional seperti Scala).

Mengapa StackOverflowError terjadi di emulator tetapi tidak di perangkat?

Ukuran tumpukan di emulator dan perangkat nyata dapat berbeda. Emulator menggunakan Desktop JVM dengan tumpukan tipikal 512–1024 KB, sedangkan Android ART — 32–48 KB. Kesalahan akan muncul di ART lebih awal daripada di Desktop JVM.

Apa perbedaan StackOverflowError dengan OutOfMemoryError?

Area memori: StackOverflowError — kesalahan tumpukan (frame panggilan), OutOfMemoryError — kesalahan heap (objek). StackOverflowError hampir selalu disebabkan oleh rekursi, sedangkan OutOfMemoryError — oleh kebocoran memori atau objek besar.

Ringkasan

  • StackOverflowError — luberan tumpukan panggilan saat melampaui batas kedalaman rekursi
  • Kedalaman tumpukan di Android adalah 512–1024 frame di thread utama
  • Rekursi tak terbatas — penyebab utama; periksa kondisi dasar di setiap metode rekursif
  • Ketergantungan siklik dalam konstruktor — penyebab luberan yang kurang jelas namun sering
  • Penggantian iteratif rekursi melalui Stack<T> eksplisit sepenuhnya menghilangkan risiko
  • tailrec di Kotlin mengubah rekursi ekor menjadi iterasi di tingkat kompiler
  • Analisis statis (Detekt, Infer) menemukan potensi rekursi tak terbatas sebelum eksekusi

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga