Stack Overflow în dezvoltarea mobilă — ce este, cauze și metode de prevenire

Autor: IT Sectr Publicat: 2026-03-29 Timp de citire: 9 min

Stack Overflow — eroarea de depășire a stivei de apeluri (java.lang.StackOverflowError), care apare la depășirea adâncimii maxime a stivei firului de execuție. Conform Java Virtual Machine Specification, adâncimea tipică a stivei în JVM este de 1024 de cadre pentru sistemele pe 64 de biți. Cauza principală — recursiunea infinită fără o condiție de bază de oprire.

Principalele puncte

  • StackOverflowError — eroare JVM la depășirea limitei de adâncime a stivei de apeluri
  • Adâncimea stivei este limitată și constituie 512–2048 de cadre în funcție de configurare
  • Recursiunea infinită — cea mai frecventă cauză a StackOverflowError
  • Recursiunea coadă nu este optimizată în JVM, spre deosebire de limbajele funcționale
  • Înlocuirea iterativă a recursiunii — o modalitate sigură de a preveni depășirea

Ce este Stack Overflow

StackOverflowError — este o eroare fatală a mașinii virtuale Java (JVM) sau Android Runtime (ART), care apare atunci când stiva de apeluri a firului de execuție atinge adâncimea maximă permisă. Spre deosebire de OutOfMemoryError (lipsă de Heap), StackOverflowError este legat de o altă zonă de memorie — stiva, unde sunt stocate cadrele de apeluri ale metodelor și variabilele locale.

Fiecare apel de metodă creează un cadru în stivă: adresa de returnare, parametrii și variabilele locale. La returnarea din metodă, cadrul este distrus. Dacă metoda se autoapelează (recursiune) fără o condiție de bază, cadrele se acumulează până la umplerea stivei. JVM nu poate aloca un nou cadru și aruncă StackOverflowError cu mesajul „null” (în Java) sau cu indicarea unui șir de stivă infinit repetat.

Dimensiunea stivei firului de execuție este fixată la creare și nu se modifică în timpul execuției. În Android, dimensiunea tipică a stivei firului principal este de 32–48 KB, ceea ce oferă o adâncime de aproximativ 512–1024 de cadre pentru metode fără un număr mare de variabile locale. Pentru firele de fundal, dimensiunea implicită este mai mică — 16–24 KB.

Cum funcționează stiva de apeluri

Stiva de apeluri (Call Stack) — este o structură de date LIFO (Last In, First Out) care gestionează ordinea de execuție a metodelor. De fiecare dată când programul apelează o metodă, JVM creează un cadru în stivă și îl plasează deasupra. La finalizarea metodei, cadrul este eliminat.

Fiecare cadru conține: operand stack (stiva de operanzi pentru instrucțiunile bytecode), array of local variables (inclusiv this), reference to constant pool și adresa de returnare. Cu cât o metodă are mai multe variabile locale, cu atât dimensiunea cadrului său este mai mare și cu atât mai puține metode pot fi apelate până la umplerea stivei. O metodă cu 10 parametri și 20 de variabile locale ocupă de aproximativ 3 ori mai mult spațiu decât o metodă fără parametri.

Pe Android ART utilizează propria implementare a stivei, diferită de Desktop JVM. ART poate crește stiva dinamic în anumite limite, dar pentru fiecare fir de execuție există totuși o limită strictă. Firul principal (firul UI) are cea mai mare stivă, deoarece întregul ciclu de viață al Activity și procesarea evenimentelor se execută pe acesta.

kotlin
// Recursiunea care duce la StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // nicio condiție de bază
}

// Apelul va duce la StackOverflowError la adâncimea ~1000
recursiveCall(0)

Cauzele principale ale depășirii stivei

Cinci scenarii tipice duc la StackOverflowError în aplicațiile mobile. Majoritatea sunt legate de recursiune, dar există și cauze mai puțin evidente.

Recursiune infinită fără condiție de bază

Cea mai frecventă cauză. Dezvoltatorul scrie o metodă recursivă fără condiție de oprire sau cu o condiție care nu devine niciodată true. Fiecare apel adaugă un cadru, iar stiva se umple după 500–2000 de iterații în funcție de dimensiunea cadrului. Exemplu tipic: calcularea factorialului n! fără verificarea n == 0.

Verificați condiția de bază la începutul fiecărei metode recursive. În Kotlin, utilizați require() sau check() pentru validarea parametrilor la start. Pentru recursiunea profundă (peste 100 de niveluri), luați în considerare înlocuirea cu o abordare iterativă.

Dependențe ciclice în constructori

Clasa A creează o instanță a B, clasa B creează o instanță a A — aceasta este o dependență ciclică în constructori. La încercarea de a crea A, se apelează constructorul B, care apelează constructorul A și așa mai departe până la StackOverflowError. Framework-urile DI (Dagger, Hilt) detectează astfel de cicluri în etapa de compilare, dar crearea manuală a obiectelor nu le prinde.

Utilizați Dependency Injection cu grafuri de dependențe: Dagger sau Koin verifică ciclurile în etapa de construire. Dacă ciclul este inevitabil, înlocuiți dependența directă cu o interfață cu inițializare lentă sau o fabrică Provider.

kotlin
// Dependență ciclică — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Soluție lenesă
class A(private val bProvider: Provider<B>)

Recursiune profundă la parcurgerea grafurilor

Parcurgerea arborelui View (ViewGroup.getChildAt()), a sistemului de fișiere sau a structurii JSON prin recursiune poate depăși limita stivei la o adâncime de peste 500–1000 de elemente. Android ViewGroup cu imbricare de 20 de niveluri este rar, dar parsarea recursivă a JSON cu 2000 de obiecte imbricate este un scenariu real.

Înlocuiți parcurgerea recursivă cu una iterativă printr-un Stack<T> explicit sau ArrayDeque. Aceasta elimină complet riscul de depășire a stivei, deoarece obiectele din heap nu sunt limitate de stivă. BFS (Breadth-First Search) prin Queue rezolvă de asemenea problema.

Procesarea incorectă a onConfigurationChanged

Cauză specifică Android: apelarea ciclică a metodelor ciclului de viață la procesarea incorectă a configurației. De exemplu, în onConfigurationChanged se apelează recreate(), care apelează din nou onConfigurationChanged și așa mai departe până la StackOverflowError. Similar: setContentView() în interiorul onLayout(), care cauzează măsurarea și layout-ul repetat.

Nu apelați recreate() în interiorul metodelor legate de modificarea configurației. Pentru actualizarea UI la schimbarea temei, utilizați setTheme() fără recreate. Pentru schimbarea dinamică a orientării — requestOrientation() o singură dată, fără flag în configurație.

Serializarea cu referințe ciclice

Gson, Moshi sau Kotlin Serialization la încercarea de a serializa un obiect cu referințe ciclice (A se referă la B, B se referă la A) intră în recursiune infinită și cad cu StackOverflowError. Aceasta este o problemă frecventă la serializarea Entity cu bidirectional Relationship (JPA, Room cu ForeignKey).

Utilizați @Transient, @JsonIgnore sau @kotlinx.serialization.Transient pentru una dintre părțile ciclului. Pentru Gson — JsonSerializer cu limitare explicită a adâncimii. Pentru Room — nu serializați niciodată Entity direct, utilizați mapper-e DTO.

Cum să diagnosticați și să reparați StackOverflowError

Diagnosticarea StackOverflowError este mai simplă decât a altor erori de memorie: stack trace în majoritatea cazurilor arată o secvență repetată de apeluri. Aceasta indică imediat recursiunea.

Citirea stack trace

Stack trace al StackOverflowError este unic: după primele 200–500 de linii începe repetarea aceluiași model de apeluri. JVM trunchiază liniile repetate la sfârșit și arată „... 1234 more”. Numărul de linii nerepetate înainte de „...” arată adâncimea recursiunii care a dus la eroare.

Citiți primele linii ale stack trace — ele arată de la ce metodă a început repetarea. Găsiți metoda care se autoapelează sau creează un lanț de apeluri care revine la ea. Corectați condiția de bază sau înlocuiți recursiunea cu o buclă.

Creșterea dimensiunii stivei (soluție temporară)

Temporar problema poate fi rezolvată prin creșterea dimensiunii stivei cu flag-ul JVM -Xss. În Android, dimensiunea stivei se setează prin AndroidManifest: android:largeHeap nu afectează stiva. Pentru a crește stiva firului de execuție în cod: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — dimensiunea dorită în octeți.

kotlin
// Crearea unui fir cu stivă mărită
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

Important: creșterea stivei nu rezolvă problema, ci doar o amână. La recursiune de 10 000 de niveluri, stiva de 64 KB va fi înlocuită cu o stivă de 128 KB, ceea ce va da 20 000 de niveluri — dar eroarea va apărea oricum, doar mai târziu. Singura soluție corectă — înlocuirea iterativă a recursiunii.

Înlocuirea recursiunii cu iterație

Algoritmii iterativi nu folosesc stiva de apeluri pentru a stoca stări intermediare — le stochează în heap (Stack<T> sau ArrayDeque). Parcurgerea arborelui binar, calcularea factorialului, Fibonacci — orice recursiune poate fi transformată în iterație printr-o stivă explicită.

kotlin
// Parcurgerea iterativă a arborelui — fără risc de 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) }
    }
}

Cum să preveniți Stack Overflow

Prevenirea StackOverflowError este un set de reguli și instrumente care detectează potențialele cicluri recursive înainte de a ajunge în producție.

Limita de adâncime a recursiunii în versiunea debug

Adăugați un contor de adâncime de protecție în metodele recursive în versiunea debug. Dacă adâncimea depășește pragul (de exemplu, 1000), aruncați o excepție cu un mesaj inteligibil. Aceasta transformă StackOverflowError cu trace ilizibil într-o excepție de business ușor de înțeles.

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("Recursiunea a depășit 1000 de niveluri")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

Analiza statică a codului

Detekt (Kotlin) și Infer (Facebook) găsesc recursiuni potențial infinite la nivel de analiză statică. Detekt are regula PotentiallyInfiniteRecursion, care avertizează despre self-call fără modificarea parametrilor. Includeți-o în setul de reguli CI și setați severity pe error.

Code Review cu focalizare pe recursiune

În code review acordați atenție: oricăror metode self-call, apeluri recursive în interiorul lambda (funcții inline Kotlin), apeluri ciclice între clase diferite, recursiune în property delegates. Pentru fiecare metodă recursivă verificați: există condiție de bază, se modifică parametrul la fiecare pas, garantează modificarea parametrului atingerea condiției de bază.

Transformarea recursiunii coadă (limitat)

Kotlin suportă modificatorul tailrec: dacă o metodă recursivă este marcată cu tailrec și apelul este de coadă (ultima operație), compilatorul o transformă în iterație. Totuși, tailrec funcționează doar pentru self-call (metoda se autoapelează direct), nu funcționează pentru recursiunea mutuală și nu este suportat în versiunile Kotlin compatibile cu Android anterioare 1.5.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // apel de coadă
}

Întrebări frecvente

Poate fi prins StackOverflowError prin try-catch?

Se poate, dar doar la nivel Java. Error, la fel ca Exception, este Throwable. Totuși, după StackOverflowError stiva este deteriorată — cadrele care nu au încăput nu se pot finaliza corect. Încercarea de a crea un obiect nou în blocul catch poate provoca un alt StackOverflowError.

Care este dimensiunea implicită a stivei în Android?

Pentru firul principal — 32–48 KB, pentru cel de fundal — 16–24 KB. Dimensiunea exactă depinde de versiunea Android și de producătorul dispozitivului. ART utilizează extinderea dinamică a stivei, dar nu mai mult de 2× față de valoarea inițială.

Poate recursiunea coadă să prevină StackOverflowError?

În Kotlin — da, dacă metoda este marcată cu tailrec. Compilatorul transformă recursiunea coadă în iterație, eliminând complet creșterea stivei. În Java, recursiunea coadă nu este optimizată de JVM (spre deosebire de limbajele funcționale precum Scala).

De ce StackOverflowError apare pe emulator dar nu pe dispozitiv?

Dimensiunea stivei pe emulator și pe dispozitivul real poate diferi. Emulatorul utilizează Desktop JVM cu o stivă tipică de 512–1024 KB, iar Android ART — 32–48 KB. Eroarea va apărea în ART mai devreme decât în Desktop JVM.

Prin ce diferă StackOverflowError de OutOfMemoryError?

Zona de memorie: StackOverflowError — eroare de stivă (cadre de apeluri), OutOfMemoryError — eroare de heap (obiecte). StackOverflowError este aproape întotdeauna cauzat de recursiune, iar OutOfMemoryError — de scurgeri de memorie sau obiecte mari.

Rezumat

  • StackOverflowError — depășirea stivei de apeluri la depășirea limitei de adâncime a recursiunii
  • Adâncimea stivei în Android este de 512–1024 de cadre pe firul principal
  • Recursiunea infinită — cauza principală; verificați condiția de bază în fiecare metodă recursivă
  • Dependențele ciclice în constructori — o cauză mai puțin evidentă dar frecventă de depășire
  • Înlocuirea iterativă a recursiunii printr-un Stack<T> explicit elimină complet riscul
  • tailrec în Kotlin transformă recursiunea coadă în iterație la nivel de compilator
  • Analiza statică (Detekt, Infer) găsește recursiuni potențial infinite înainte de execuție

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și