Stack Overflow — fel vid överflöde av anropsstacken (java.lang.StackOverflowError), som uppstår när den maximala stackdjupet för en tråd överskrids. Enligt Java Virtual Machine Specification är det typiska stackdjupet i JVM 1024 ramar för 64-bitars system. Den främsta orsaken är oändlig rekursion utan basvillkor för stopp.
Huvudpunkter
StackOverflowError — är ett dödligt fel i Java Virtual Machine (JVM) eller Android Runtime (ART), som uppstår när en tråds anropsstack når det maximalt tillåtna djupet. Till skillnad från OutOfMemoryError (brist på Heap) är StackOverflowError kopplat till ett annat minnesområde — stacken, där ramar för metodanrop och lokala variabler lagras.
Varje metodanrop skapar en ram i stacken: returadress, parametrar och lokala variabler. Vid återvändo från metoden förstörs ramen. Om en metod anropar sig själv (rekursion) utan basvillkor, ackumuleras ramarna tills stacken är full. JVM kan inte allokera en ny ram och kastar StackOverflowError med meddelandet „null” (i Java) eller med indikation på en oändligt upprepande stacksträng.
Storleken på trådens stack är fastställd vid skapandet och ändras inte under exekvering. I Android är den typiska stackstorleken för huvudtråden 32–48 KB, vilket ger ett djup på cirka 512–1024 ramar för metoder utan många lokala variabler. För bakgrundstrådar är standardstorleken mindre — 16–24 KB.
Anropsstacken (Call Stack) — är en LIFO-datastruktur (Last In, First Out) som hanterar exekveringsordningen för metoder. Varje gång programmet anropar en metod skapar JVM en ram i stacken och placerar den överst. När metoden slutförs tas ramen bort.
Varje ram innehåller: operand stack (operandstack för bytekodinstruktioner), array of local variables (inklusive this), reference to constant pool och returadress. Ju fler lokala variabler en metod har, desto större är dess ramstorlek och desto färre metoder kan anropas innan stacken är full. En metod med 10 parametrar och 20 lokala variabler tar ungefär 3 gånger mer plats än en metod utan parametrar.
På Android använder ART sin egen stackimplementering, som skiljer sig från Desktop JVM. ART kan dynamiskt öka stacken inom vissa gränser, men för varje tråd finns fortfarande en hård gräns. Huvudtråden (UI-tråden) har den största stacken eftersom hela Activitys livscykel och händelsehantering exekveras på den.
// Rekursion som leder till StackOverflowError
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // inget basvillkor
}
// Anropet kommer att leda till StackOverflowError på djup ~1000
recursiveCall(0)
Fem typiska scenarier leder till StackOverflowError i mobilapplikationer. De flesta är relaterade till rekursion, men det finns också mindre uppenbara orsaker.
Den vanligaste orsaken. Utvecklaren skriver en rekursiv metod utan stoppvillkor eller med ett villkor som aldrig blir true. Varje anrop lägger till en ram och stacken fylls efter 500–2000 iterationer beroende på ramstorlek. Typiskt exempel: beräkning av fakulteten n! utan kontroll av n == 0.
Kontrollera basvillkoret i början av varje rekursiv metod. I Kotlin, använd require() eller check() för validering av parametrar vid start. För djup rekursion (mer än 100 nivåer) överväg ersättning med iterativ metod.
Klass A skapar en instans av B, klass B skapar en instans av A — detta är ett cyklistiskt beroende i konstruktorer. Vid försök att skapa A anropas B:s konstruktor, som anropar A:s konstruktor, och så vidare till StackOverflowError. DI-ramverk (Dagger, Hilt) upptäcker sådana cykler i kompileringsfasen, men manuellt objektskapande fångar dem inte.
Använd Dependency Injection med beroendegrafer: Dagger eller Koin kontrollerar cykler i byggfasen. Om en cykel är oundviklig, ersätt det direkta beroendet med ett gränssnitt med lazy-initiering eller en Provider-fabrik.
// Cyklistiskt beroende — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Lazy-lösning
class A(private val bProvider: Provider<B>)
Genomgång av View-trädet (ViewGroup.getChildAt()), filsystemet eller JSON-strukturen via rekursion kan överskrida stackgränsen vid ett djup på mer än 500–1000 element. Android ViewGroup med 20 nivåers nästling är sällsynt, men rekursiv tolkning av JSON med 2000 nästlade objekt är ett realistiskt scenario.
Ersätt rekursiv genomgång med iterativ via en explicit Stack<T> eller ArrayDeque. Detta eliminerar helt risken för stacköverflöde eftersom objekt i heapen inte begränsas av stackgränsen. BFS (Breadth-First Search) via Queue löser också problemet.
Android-specifik orsak: cyklistisk anrop av livscykelmetoder vid felaktig konfigurationshantering. Till exempel anropas recreate() i onConfigurationChanged, som återigen anropar onConfigurationChanged, och så vidare till StackOverflowError. Liknande: setContentView() inuti onLayout(), som orsakar upprepad mätning och layout.
Anropa inte recreate() inuti metoder relaterade till konfigurationsändring. För UI-uppdatering vid temabyte, använd setTheme() utan recreate. För dynamisk orienteringsändring — requestOrientation() en gång, utan flagga i konfigurationen.
Gson, Moshi eller Kotlin Serialization vid försök att serialisera ett objekt med cykliska referenser (A refererar till B, B refererar till A) hamnar i oändlig rekursion och kraschar med StackOverflowError. Detta är ett vanligt problem vid serialisering av Entity med bidirectional Relationship (JPA, Room med ForeignKey).
Använd @Transient, @JsonIgnore eller @kotlinx.serialization.Transient för ena sidan av cykeln. För Gson — JsonSerializer med explicit djupbegränsning. För Room — serialisera aldrig Entity direkt, använd DTO-mappare.
Diagnostik av StackOverflowError är enklare än andra minnesfel: stacktrace visar i de flesta fall en upprepande sekvens av anrop. Detta pekar omedelbart på rekursion.
Stacktrace för StackOverflowError är unik: efter de första 200–500 raderna börjar upprepning av samma anropsmönster. JVM kapar upprepande rader i slutet och visar „... 1234 more”. Antalet icke-upprepande rader före „...” visar djupet av rekursionen som ledde till felet.
Läs de första raderna av stacktrace — de visar från vilken metod upprepningen började. Hitta metoden som anropar sig själv eller skapar en anropskedja som återvänder till den. Korrigera basvillkoret eller ersätt rekursionen med en loop.
Tillfälligt kan problemet lösas genom att öka stackstorleken med JVM-flaggan -Xss. I Android ställs stackstorleken in via AndroidManifest: android:largeHeap påverkar inte stacken. För att öka trådens stack i kod: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — önskad storlek i byte.
// Skapa tråd med förstorad stack
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Viktigt: att öka stacken löser inte problemet, bara fördröjer det. Vid rekursion på 10 000 nivåer kommer en 64 KB stack att ersättas med en 128 KB stack, vilket ger 20 000 nivåer — men felet kommer ändå att inträffa, bara senare. Den enda korrekta lösningen — iterativ ersättning av rekursion.
Iterativa algoritmer använder inte anropsstacken för att lagra mellanliggande tillstånd — de lagrar dem i heapen (Stack<T> eller ArrayDeque). Genomgång av binärt träd, fakultetsberäkning, Fibonacci — vilken rekursion som helst kan omvandlas till iteration via en explicit stack.
// Iterativ trädgenomgång — utan risk för 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) }
}
}
Förebyggande av StackOverflowError är en uppsättning regler och verktyg som upptäcker potentiella rekursiva cykler innan de når produktion.
Lägg till en skyddande djupräknare i rekursiva metoder i debug-bygget. Om djupet överskrider tröskeln (t.ex. 1000), kasta ett undantag med ett tydligt meddelande. Detta förvandlar StackOverflowError med oläslig trace till ett förståeligt affärsundantag.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("Rekursionen överskred 1000 nivåer")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) och Infer (Facebook) hittar potentiellt oändliga rekursioner på statisk analysnivå. Detekt har regeln PotentiallyInfiniteRecursion som varnar om self-call utan parameterändring. Inkludera den i CI-regeluppsättningen och ställ in severity på error.
Vid code review var uppmärksam på: alla self-call-metoder, rekursiva anrop inuti lambdas (Kotlin inline-funktioner), cykliska anrop mellan olika klasser, rekursion i property delegates. För varje rekursiv metod kontrollera: finns basvillkor, ändras parametern vid varje steg, garanterar parameterändringen att basvillkoret nås.
Kotlin stöder tailrec-modifieraren: om en rekursiv metod är märkt med tailrec och anropet är ett svansanrop (sista operationen), omvandlar kompilatorn den till iteration. Dock fungerar tailrec endast för self-call (metoden anropar sig själv direkt), fungerar inte för ömsesidig rekursion och stöds inte i Android-kompatibla Kotlin-versioner före 1.5.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // svansanrop
}
Vanliga frågor
Ja, men endast på Java-nivå. Error, precis som Exception, är Throwable. Men efter StackOverflowError är stacken skadad — ramar som inte fick plats kan inte slutföras korrekt. Försök att skapa ett nytt objekt i catch-blocket kan orsaka en ny StackOverflowError.
För huvudtråden — 32–48 KB, för bakgrundstråden — 16–24 KB. Den exakta storleken beror på Android-versionen och enhetstillverkaren. ART använder dynamisk stackutvidgning, men inte mer än 2× från startvärdet.
I Kotlin — ja, om metoden är märkt med tailrec. Kompilatorn omvandlar svansrekursion till iteration, vilket helt eliminerar stacktillväxt. I Java optimeras inte svansrekursion av JVM (till skillnad från funktionella språk som Scala).
Stackstorleken på emulatorn och den verkliga enheten kan skilja sig. Emulatorn använder Desktop JVM med en typisk stack på 512–1024 KB, medan Android ART har 32–48 KB. Felet kommer att visa sig i ART tidigare än i Desktop JVM.
Minnesområde: StackOverflowError — stackfel (anropsramar), OutOfMemoryError — heapfel (objekt). StackOverflowError orsakas nästan alltid av rekursion, medan OutOfMemoryError orsakas av minnesläckor eller stora objekt.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också