Stack Overflow, bir iş parçacığının maksimum yığın derinliği aşıldığında ortaya çıkan bir çağrı yığını taşması hatasıdır (java.lang.StackOverflowError). Java Virtual Machine Specification'a göre, 64-bit sistemler için JVM'de tipik yığın derinliği 1024 çerçevedir. Ana neden, temel koşulu olmayan sonsuz özyinelemedir.
Önemli Noktalar
StackOverflowError, bir iş parçacığının çağrı yığını izin verilen maksimum derinliğe ulaştığında ortaya çıkan Java Sanal Makinesi (JVM) veya Android Runtime (ART) ölümcül hatasıdır. OutOfMemoryError'dan (Heap tükenmesi) farklı olarak StackOverflowError, belleğin farklı bir alanıyla — yöntem çağrı çerçevelerinin ve yerel değişkenlerin depolandığı yığınla ilgilidir.
Her yöntem çağrısı yığında bir çerçeve oluşturur: dönüş adresi, parametreler ve yerel değişkenler. Yöntemden dönüşte çerçeve yok edilir. Bir yöntem temel koşul olmadan kendini çağırırsa (özyineleme), yığın dolana kadar çerçeveler birikir. JVM yeni bir çerçeve ayıramaz ve «null» mesajıyla (Java'da) veya sonsuzca tekrar eden bir yığın satırı göstergesiyle StackOverflowError fırlatır.
Bir iş parçacığının yığın boyutu oluşturulurken sabitlenir ve yürütme sırasında değişmez. Android'de, ana iş parçacığının tipik yığın boyutu 32–48 KB'dir ve bu, çok fazla yerel değişkeni olmayan yöntemler için yaklaşık 512–1024 çerçeve derinliği sağlar. Arka plan iş parçacıkları için varsayılan boyut daha küçüktür — 16–24 KB.
Çağrı yığını (Call Stack), yöntem yürütme sırasını yöneten bir LIFO (Last In, First Out) veri yapısıdır. Program bir yöntemi her çağırdığında, JVM yığında bir çerçeve oluşturur ve onu en üste yerleştirir. Yöntem tamamlandığında çerçeve çıkarılır.
Her çerçeve şunları içerir: işlenen yığını (bayt kodu talimatları için), yerel değişkenler dizisi (this dahil), sabit havuzuna referans ve dönüş adresi. Bir yöntemin ne kadar çok yerel değişkeni varsa, çerçeve boyutu o kadar büyük olur ve yığın dolmadan önce o kadar az yöntem çağrılabilir. 10 parametreli ve 20 yerel değişkenli bir yöntem, parametresiz bir yöntemden yaklaşık 3 kat daha fazla yer kaplar.
Android'de, ART masaüstü JVM'den farklı olarak kendi yığın uygulamasını kullanır. ART, belirli sınırlar içinde yığını dinamik olarak artırabilir, ancak her iş parçacığı için hala sabit bir sınır vardır. Ana iş parçacığı (UI iş parçacığı), tüm Activity yaşam döngüsünü ve olay işlemeyi yönettiği için en büyük yığına sahiptir.
// StackOverflowError'a yol açan özyineleme
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // temel koşul yok
}
// Çağrı ~1000 derinliğinde StackOverflowError'a neden olur
recursiveCall(0)
Beş tipik senaryo mobil uygulamalarda StackOverflowError'a yol açar. Çoğu özyineleme ile ilgilidir, ancak daha az belirgin nedenler de vardır.
En yaygın neden. Geliştirici, durma koşulu olmayan veya asla true olmayan bir koşula sahip özyinelemeli bir yöntem yazar. Her çağrı bir çerçeve ekler ve yığın, çerçeve boyutuna bağlı olarak 500–2000 yinelemede dolar. Tipik bir örnek: n == 0'ı kontrol etmeden n! faktöriyelini hesaplamak.
Her özyinelemeli yöntemin başında temel koşulu kontrol edin. Kotlin'de, parametre doğrulaması için require() veya check() kullanın. Derin özyineleme (100 seviyeden fazla) için yinelemeli bir yaklaşımla değiştirmeyi düşünün.
A Sınıfı B'nin bir örneğini oluşturur, B sınıfı A'nın bir örneğini oluşturur — bu, yapıcılarda döngüsel bir bağımlılıktır. A'yı oluşturmaya çalışırken, B'nin yapıcısı çağrılır, bu da A'nın yapıcısını çağırır ve StackOverflowError'a kadar böyle devam eder. DI çerçeveleri (Dagger, Hilt) bu tür döngüleri derleme zamanında tespit eder, ancak manuel nesne oluşturma bunları yakalamaz.
Bağımlılık grafikleriyle Bağımlılık Enjeksiyonu kullanın: Dagger veya Koin, derleme zamanında döngüleri kontrol eder. Döngü kaçınılmazsa, doğrudan bağımlılığı tembel başlatma veya Provider fabrikası olan bir arayüzle değiştirin.
// Döngüsel bağımlılık — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Tembel çözüm
class A(private val bProvider: Provider<B>)
Özyineleme yoluyla bir View ağacı (ViewGroup.getChildAt()), dosya sistemi veya JSON yapısının geçişi, 500–1000 öğeden daha fazla derinlikte yığın sınırını aşabilir. 20 seviye iç içe geçmiş bir Android ViewGroup nadirdir, ancak 2000 iç içe nesneyle özyinelemeli JSON ayrıştırma gerçek bir senaryodur.
Özyinelemeli geçişi, açık bir Stack<T> veya ArrayDeque kullanarak yinelemeli geçişle değiştirin. Bu, yığın taşması riskini tamamen ortadan kaldırır çünkü yığın nesneleri yığın sınırıyla sınırlı değildir. Queue aracılığıyla BFS (Breadth-First Search) de sorunu çözer.
Android'e özgü bir neden: yapılandırma yanlış işlendiğinde yaşam döngüsü yöntemlerinin döngüsel çağrısı. Örneğin, onConfigurationChanged içinde recreate() çağırmak, tekrar onConfigurationChanged'i çağırır ve StackOverflowError'a kadar böyle devam eder. Benzer şekilde: onLayout() içinde setContentView(), başka bir ölçüm ve düzeni tetikler.
Yapılandırma değişiklikleriyle ilgili yöntemlerin içinde recreate() çağırmayın. Tema değişikliğinde UI'yi güncellemek için recreate olmadan setTheme() kullanın. Dinamik yön değişiklikleri için — yapılandırmada bir işaret olmadan requestOrientation()'ı bir kez çağırın.
Gson, Moshi veya Kotlin Serialization, döngüsel referanslara (A, B'yi referans alır, B, A'yı referans alır) sahip bir nesneyi serileştirmeye çalışırken sonsuz özyinelemeye girer ve StackOverflowError ile çöker. Bu, çift yönlü ilişkilere (JPA, ForeignKey ile Room) sahip Entity'leri serileştirirken yaygın bir sorundur.
Döngünün bir tarafında @Transient, @JsonIgnore veya @kotlinx.serialization.Transient kullanın. Gson için — açık derinlik sınırı olan JsonSerializer. Room için — Entity'yi asla doğrudan serileştirmeyin, DTO eşleyicileri kullanın.
StackOverflowError teşhisi diğer bellek hatalarından daha kolaydır: yığın izi çoğu durumda tekrar eden bir çağrı dizisi gösterir. Bu hemen özyinelemeyi işaret eder.
StackOverflowError'un yığın izi benzersizdir: ilk 200–500 satırdan sonra aynı çağrı deseni tekrarlanmaya başlar. JVM, sonunda tekrar eden satırları keser ve «... 1234 more» gösterir. «...» öncesindeki tekrarlanmayan satırların sayısı, hataya neden olan özyineleme derinliğini gösterir.
Yığın izinin ilk satırlarını okuyun — tekrarın hangi yöntemle başladığını gösterirler. Kendini çağıran veya kendisine dönen bir çağrı zinciri oluşturan yöntemi bulun. Temel koşulu düzeltin veya özyinelemeyi bir döngüyle değiştirin.
Geçici olarak, sorun JVM bayrağı -Xss ile yığın boyutunu artırarak çözülebilir. Android için yığın boyutu AndroidManifest aracılığıyla ayarlanır: android:largeHeap yığını etkilemez. Kodda iş parçacığı yığınını artırmak için: Thread(ThreadGroup, Runnable, name, stackSize). stackSize, bayt cinsinden istenen boyuttur.
// Artırılmış yığınla iş parçacığı oluşturma
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Önemli: yığını artırmak sorunu çözmez, yalnızca geciktirir. 10.000 seviyeli özyinelemede, 64 KB'lik bir yığın 128 KB'lik bir yığınla değiştirilir ve 20.000 seviye verir — ancak hata yine de oluşur, sadece daha sonra. Tek doğru çözüm, özyinelemenin yinelemeli olarak değiştirilmesidir.
Yinelemeli algoritmalar, ara durumları depolamak için çağrı yığınını kullanmaz — onları yığında (Stack<T> veya ArrayDeque) depolar. İkili ağaç geçişi, faktöriyel hesaplama, Fibonacci — herhangi bir özyineleme, açık bir yığın kullanılarak yinelemeye dönüştürülebilir.
// Yinelemeli ağaç geçişi — StackOverflow riski yok
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) }
}
}
StackOverflowError'u önleme, potansiyel özyinelemeli döngüleri üretime ulaşmadan önce tanımlayan bir dizi kural ve araçtır.
Hata ayıklama yapılarında özyinelemeli yöntemlere koruyucu bir derinlik sayacı ekleyin. Derinlik bir eşiği (örneğin, 1000) aşarsa, net bir mesajla bir istisna fırlatın. Bu, okunamaz bir iz ile StackOverflowError'u anlaşılabilir bir iş istisnasına dönüştürür.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("Özyineleme 1000 seviyeyi aştı")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) ve Infer (Facebook), statik analiz seviyesinde potansiyel sonsuz özyinelemeleri bulur. Detekt, parametreleri değiştirmeden kendi kendine çağrı hakkında uyaran PotentiallyInfiniteRecursion kuralına sahiptir. CI kural setinde etkinleştirin ve ciddiyeti error olarak ayarlayın.
Kod incelemesinde şunlara dikkat edin: kendi kendine çağrı yapan yöntemler, lambda içindeki özyinelemeli çağrılar (Kotlin satır içi işlevleri), farklı sınıflar arasındaki döngüsel çağrılar, özellik temsilcilerinde özyineleme. Her özyinelemeli yöntem için kontrol edin: temel koşul var mı, parametre her adımda değişiyor mu, parametre değişikliği temel koşula ulaşmayı garanti ediyor mu.
Kotlin tailrec değiştiricisini destekler: özyinelemeli bir yöntem tailrec ile işaretlenmişse ve çağrı kuyruk (son işlem) ise, derleyici onu yinelemeye dönüştürür. Ancak tailrec yalnızca kendi kendine çağrı (yöntem doğrudan kendini çağırır) için çalışır, karşılıklı özyineleme için çalışmaz ve Android uyumlu Kotlin sürüm 1.5'ten önce desteklenmez.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // kuyruk çağrısı
}
Sıkça Sorulan Sorular
Evet, ancak yalnızca Java seviyesinde. Error, Exception gibi, bir Throwable'dır. Ancak StackOverflowError'dan sonra yığın hasar görür — sığmayan çerçeveler doğru şekilde tamamlanamaz. catch bloğunda yeni bir nesne oluşturma girişimi başka bir StackOverflowError'a neden olabilir.
Ana iş parçacığı için — 32–48 KB, arka plan iş parçacıkları için — 16–24 KB. Kesin boyut Android sürümüne ve cihaz üreticisine bağlıdır. ART dinamik yığın genişletme kullanır ancak başlangıç değerinin 2 katından fazla olmaz.
Kotlin'de — evet, yöntem tailrec ile işaretlenmişse. Derleyici kuyruk özyinelemesini yinelemeye dönüştürür ve yığın büyümesini tamamen ortadan kaldırır. Java'da, kuyruk özyinelemesi JVM tarafından optimize edilmez (Scala gibi işlevsel dillerin aksine).
Yığın boyutu öykünücü ve gerçek cihazda farklılık gösterebilir. Öykünücü, tipik 512–1024 KB yığınlı masaüstü JVM kullanırken, Android ART 32–48 KB kullanır. Hata, masaüstü JVM'den önce ART'de ortaya çıkacaktır.
Bellek alanı: StackOverflowError yığın hatası (çağrı çerçeveleri), OutOfMemoryError yığın (heap) hatasıdır (nesneler). StackOverflowError neredeyse her zaman özyinelemeden kaynaklanırken, OutOfMemoryError bellek sızıntılarından veya büyük nesnelerden kaynaklanır.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun