Stack Overflow — a hívási verem túlcsordulási hibája (java.lang.StackOverflowError), amely a szálverem maximális mélységének túllépésekor keletkezik. A Java Virtual Machine Specification szerint a JVM tipikus vermének mélysége 1024 keret a 64 bites rendszereknél. A fő ok — a végtelen rekurzió alapmegállási feltétel nélkül.
Főbb pontok
StackOverflowError — a Java Virtual Machine (JVM) vagy Android Runtime (ART) végzetes hibája, amely akkor következik be, amikor a szál hívási verme eléri a maximális megengedett mélységet. Az OutOfMemoryError-tól (Heap hiány) eltérően a StackOverflowError a memória egy másik területéhez — a veremhez — kapcsolódik, ahol a metódushívások keretei és a lokális változók tárolódnak.
Minden metódushívás létrehoz egy keretet a veremben: visszatérési címet, paramétereket és lokális változókat. A metódusból való visszatéréskor a keret megsemmisül. Ha a metódus meghívja önmagát (rekurzió) alapfeltétel nélkül, a keretek felhalmozódnak a verem telítődéséig. A JVM nem tud új keretet allokálni, és StackOverflowError-t dob a „null” üzenettel (Java-ban) vagy a végtelenül ismétlődő vermen nyom jelzésével.
A szál vermének mérete a létrehozáskor rögzített, és futás közben nem változik. Androidban a fő szál tipikus vermének mérete 32–48 KB, ami körülbelül 512–1024 keret mélységet biztosít a sok lokális változóval nem rendelkező metódusok számára. A háttérszálak esetében az alapértelmezett méret kisebb — 16–24 KB.
A hívási verem (Call Stack) — egy LIFO (Last In, First Out) adatstruktúra, amely a metódusok végrehajtási sorrendjét kezeli. Minden alkalommal, amikor a program meghív egy metódust, a JVM létrehoz egy keretet a veremben, és a tetejére helyezi. A metódus befejezésekor a keret eltávolításra kerül.
Minden keret tartalmazza: operand stack (operandus verem a bájtkód utasításokhoz), array of local variables (beleértve a this-t), reference to constant pool és visszatérési címet. Minél több lokális változója van egy metódusnak, annál nagyobb a keretének mérete, és annál kevesebb metódus hívható meg a verem telítődése előtt. Egy 10 paraméterrel és 20 lokális változóval rendelkező metódus körülbelül 3-szor több helyet foglal, mint egy paraméter nélküli metódus.
Androidon az ART saját veremimplementációt használ, amely eltér a Desktop JVM-től. Az ART bizonyos határok között dinamikusan növelheti a vermet, de minden szál esetében továbbra is szigorú korlát van. A fő szálnak (UI szál) van a legnagyobb verme, mivel rajta fut az Activity teljes életciklusa és az eseményfeldolgozás.
// StackOverflowError-hoz vezető rekurzió
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // nincs alapfeltétel
}
// A hívás ~1000 mélységben StackOverflowError-hoz vezet
recursiveCall(0)
Öt tipikus forgatókönyv vezet StackOverflowError-hoz mobilalkalmazásokban. A legtöbbjük a rekurzióhoz kapcsolódik, de vannak kevésbé nyilvánvaló okok is.
A leggyakoribb ok. A fejlesztő rekurzív metódust ír megállási feltétel nélkül vagy olyan feltétellel, amely soha nem lesz true. Minden hívás hozzáad egy keretet, és a verem 500–2000 iteráció után telik meg a keret méretétől függően. Tipikus példa: az n! faktoriális kiszámítása az n == 0 ellenőrzése nélkül.
Ellenőrizze az alapfeltételt minden rekurzív metódus elején. Kotlinban használja a require() vagy check() függvényeket a paraméterek érvényesítésére induláskor. Mély rekurzió esetén (több mint 100 szint) fontolja meg az iteratív megközelítéssel való helyettesítést.
Az A osztály létrehozza B példányát, a B osztály létrehozza A példányát — ez egy ciklikus függőség a konstruktorokban. Az A létrehozásakor meghívódik B konstruktora, amely meghívja A konstruktorát, és így tovább a StackOverflowError-ig. A DI keretrendszerek (Dagger, Hilt) észlelik az ilyen ciklusokat a fordítási fázisban, de a kézi objektumlétrehozás nem fogja fel őket.
Használjon Dependency Injection-t függőségi gráfokkal: a Dagger vagy Koin ellenőrzi a ciklusokat a build fázisban. Ha a ciklus elkerülhetetlen, cserélje ki a közvetlen függőséget egy lusta inicializálású interfészre vagy Provider gyárra.
// Ciklikus függőség — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Lusta megoldás
class A(private val bProvider: Provider<B>)
A View fa (ViewGroup.getChildAt()), fájlrendszer vagy JSON struktúra bejárása rekurzióval 500–1000 elemnél nagyobb mélységben túllépheti a verem korlátját. A 20 szintű egymásba ágyazott Android ViewGroup ritka, de a 2000 egymásba ágyazott objektumot tartalmazó JSON rekurzív feldolgozása valós forgatókönyv.
Cserélje ki a rekurzív bejárást iteratívra egy explicit Stack<T> vagy ArrayDeque segítségével. Ez teljesen kiküszöböli a verem túlcsordulásának kockázatát, mivel a heap-ben lévő objektumokat nem korlátozza a verem korlátja. A Queue-n keresztüli BFS (Breadth-First Search) is megoldja a problémát.
Android-specifikus ok: az életciklus metódusok ciklikus meghívása a konfiguráció helytelen kezelésekor. Például az onConfigurationChanged-ben meghívódik a recreate(), amely újra meghívja az onConfigurationChanged-et, és így tovább a StackOverflowError-ig. Hasonló: setContentView() az onLayout()-on belül, ami ismételt mérést és layout-ot okoz.
Ne hívja meg a recreate()-t a konfigurációváltozással kapcsolatos metódusokon belül. A téma váltásakor a UI frissítéséhez használja a setTheme()-et recreate nélkül. Dinamikus orientációváltáshoz — requestOrientation() egyszer, flag nélkül a konfigurációban.
Gson, Moshi vagy Kotlin Serialization egy ciklikus hivatkozásokkal rendelkező objektum (A hivatkozik B-re, B hivatkozik A-ra) szerializálásakor végtelen rekurzióba kerül, és StackOverflowError-ral összeomlik. Ez gyakori probléma az Entity szerializálásakor bidirectional Relationship (JPA, Room ForeignKey) esetén.
Használja a @Transient, @JsonIgnore vagy @kotlinx.serialization.Transient annotációkat a ciklus egyik oldalán. Gson esetén — JsonSerializer explicit mélységkorlátozással. Room esetén — soha ne szerializálja közvetlenül az Entity-t, használjon DTO mapper-eket.
A StackOverflowError diagnosztizálása egyszerűbb, mint más memóriahibáké: a stack trace a legtöbb esetben ismétlődő hívási sorozatot mutat. Ez azonnal rekurzióra utal.
A StackOverflowError stack trace-e egyedi: az első 200–500 sor után ugyanazon hívási minta ismétlődése kezdődik. A JVM levágja az ismétlődő sorokat a végén, és „... 1234 more”-t jelenít meg. A „...” előtti nem ismétlődő sorok száma mutatja a hibához vezető rekurzió mélységét.
Olvassa el a stack trace első sorait — ezek mutatják, hogy melyik metódustól kezdődött az ismétlődés. Keresse meg azt a metódust, amely meghívja önmagát, vagy olyan hívási láncot hoz létre, amely visszatér hozzá. Javítsa ki az alapfeltételt, vagy cserélje ki a rekurziót ciklusra.
Átmenetileg a probléma megoldható a verem méretének növelésével a JVM -Xss kapcsolóján keresztül. Androidban a verem mérete az AndroidManifest-en keresztül állítható be: az android:largeHeap nem befolyásolja a vermet. A szál vermének kódban történő növeléséhez: Thread(ThreadGroup, Runnable, name, stackSize). A stackSize — a kívánt méret byte-ban.
// Szál létrehozása megnövelt veremmel
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Fontos: a verem növelése nem oldja meg a problémát, csak késlelteti. 10 000 szintű rekurziónál a 64 KB-os verem helyett 128 KB-os verem lesz, ami 20 000 szintet ad — de a hiba továbbra is bekövetkezik, csak később. Az egyetlen helyes megoldás — a rekurzió iteratív helyettesítése.
Az iteratív algoritmusok nem használják a hívási vermet köztes állapotok tárolására — azokat a heap-ben tárolják (Stack<T> vagy ArrayDeque). Bináris fa bejárása, faktoriális számítás, Fibonacci — bármely rekurzió átalakítható iterációvá egy explicit verem segítségével.
// Fa iteratív bejárása — StackOverflow kockázat nélkül
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) }
}
}
A StackOverflowError megelőzése olyan szabályok és eszközök összessége, amelyek felismerik a potenciális rekurzív ciklusokat, mielőtt azok éles környezetbe kerülnének.
Adjon hozzá egy védő mélységszámlálót a rekurzív metódusokhoz a debug build-ben. Ha a mélység meghaladja a küszöbértéket (pl. 1000), dobjon egy kivételt érthető üzenettel. Ez egy olvashatatlan trace-sel rendelkező StackOverflowError-t egy érthető üzleti kivétellé alakít.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("A rekurzió meghaladta az 1000 szintet")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
A Detekt (Kotlin) és az Infer (Facebook) potenciálisan végtelen rekurziókat talál a statikus analízis szintjén. A Detekt rendelkezik a PotentiallyInfiniteRecursion szabállyal, amely figyelmeztet a paraméterváltoztatás nélküli self-call-ra. Vegye fel a CI szabálykészletébe, és állítsa a severity-t error-ra.
Code review során figyeljen: minden self-call metódusra, rekurzív hívásokra lambda-n belül (Kotlin inline függvények), ciklikus hívásokra különböző osztályok között, rekurzióra a property delegates-ben. Minden rekurzív metódusnál ellenőrizze: van-e alapfeltétel, változik-e a paraméter minden lépésben, garantálja-e a paraméter változása az alapfeltétel elérését.
A Kotlin támogatja a tailrec módosítót: ha egy rekurzív metódus tailrec-vel van jelölve, és a hívás farokhívás (utolsó művelet), a fordító iterációvá alakítja. Azonban a tailrec csak self-call esetén működik (a metódus közvetlenül hívja meg önmagát), nem működik kölcsönös rekurzió esetén, és nem támogatott az Android-kompatibilis Kotlin verziókban 1.5 előtt.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // farokhívás
}
Gyakran ismételt kérdések
Igen, de csak Java szinten. Az Error, akárcsak az Exception, Throwable. Azonban a StackOverflowError után a verem sérült — a keretek, amelyek nem fértek el, nem tudnak megfelelően befejeződni. Egy új objektum létrehozása a catch blokkban újabb StackOverflowError-t okozhat.
A fő szál esetében — 32–48 KB, a háttérszál esetében — 16–24 KB. A pontos méret az Android verziójától és a készülék gyártójától függ. Az ART dinamikus verembővítést használ, de legfeljebb 2× a kezdeti értéktől.
Kotlinban — igen, ha a metódus tailrec-vel van jelölve. A fordító a farokrekurziót iterációvá alakítja, teljesen kiküszöbölve a verem növekedését. Java-ban a farokrekurzió nem optimalizálódik a JVM által (ellentétben a funkcionális nyelvekkel, mint a Scala).
A verem mérete az emulátoron és a valódi készüléken eltérhet. Az emulátor Desktop JVM-et használ, tipikus 512–1024 KB-os veremmel, míg az Android ART 32–48 KB-ot. A hiba az ART-ban hamarabb jelentkezik, mint a Desktop JVM-ben.
Memóriaterület: a StackOverflowError — veremhiba (hívási keretek), az OutOfMemoryError — heap hiba (objektumok). A StackOverflowError szinte mindig rekurzió okozza, míg az OutOfMemoryError-t memóriaszivárgás vagy nagy objektumok.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.