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 — 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.
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.
// 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)
Lima skenario tipikal menyebabkan StackOverflowError di aplikasi mobile. Sebagian besar terkait dengan rekursi, tetapi ada juga penyebab yang kurang jelas.
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.
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.
// Ketergantungan siklik — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Solusi malas
class A(private val bProvider: Provider<B>)
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.
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.
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.
Diagnosis StackOverflowError lebih sederhana daripada kesalahan memori lainnya: stack trace dalam大多数 kasus menunjukkan urutan panggilan yang berulang. Ini segera menunjukkan rekursi.
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.
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.
// 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.
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.
// 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) }
}
}
Pencegahan StackOverflowError adalah seperangkat aturan dan alat yang mendeteksi potensi siklus rekursif sebelum masuk ke produksi.
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.
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)
}
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.
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.
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.
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
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.
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.
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).
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.
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
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.
Baca juga