Stack Overflow v mobilním vývoji — co to je, příčiny a metody prevence

Autor: IT Sectr Publikováno: 2026-03-29 Doba čtení: 9 min

Stack Overflow — chyba přetečení zásobníku volání (java.lang.StackOverflowError), ke které dochází při překročení maximální hloubky zásobníku vlákna. Podle Java Virtual Machine Specification je typická hloubka zásobníku v JVM 1024 rámců pro 64bitové systémy. Hlavní příčinou je nekonečná rekurze bez základní podmínky zastavení.

Hlavní body

  • StackOverflowError — chyba JVM při překročení limitu hloubky zásobníku volání
  • Hloubka zásobníku je omezená a činí 512–2048 rámců v závislosti na konfiguraci
  • Nekonečná rekurze — nejčastější příčina StackOverflowError
  • Ocasní rekurze není v JVM optimalizována, na rozdíl od funkcionálních jazyků
  • Iterativní náhrada rekurze — spolehlivý způsob prevence přetečení

Co je Stack Overflow

StackOverflowError — je fatální chyba Java Virtual Machine (JVM) nebo Android Runtime (ART), ke které dochází, když zásobník volání vlákna dosáhne maximální povolené hloubky. Na rozdíl od OutOfMemoryError (nedostatek Heap), StackOverflowError souvisí s jinou oblastí paměti — zásobníkem, kde jsou uloženy rámce volání metod a lokální proměnné.

Každé volání metody vytváří rámec v zásobníku: návratovou adresu, parametry a lokální proměnné. Při návratu z metody je rámec zničen. Pokud metoda volá sama sebe (rekurze) bez základní podmínky, rámce se hromadí až do zaplnění zásobníku. JVM nemůže přidělit nový rámec a vyhazuje StackOverflowError se zprávou „null” (v Javě) nebo s uvedením nekonečně se opakujícího řetězce zásobníku.

Velikost zásobníku vlákna je pevně stanovena při vytvoření a během provádění se nemění. V Androidu je typická velikost zásobníku hlavního vlákna 32–48 KB, což poskytuje hloubku přibližně 512–1024 rámců pro metody bez velkého množství lokálních proměnných. Pro vlákna na pozadí je výchozí velikost menší — 16–24 KB.

Jak funguje zásobník volání

Zásobník volání (Call Stack) — je datová struktura LIFO (Last In, First Out), která řídí pořadí provádění metod. Pokaždé, když program zavolá metodu, JVM vytvoří rámec v zásobníku a umístí jej na vrchol. Po dokončení metody je rámec odstraněn.

Každý rámec obsahuje: operand stack (zásobník operandů pro instrukce bajtkódu), array of local variables (včetně this), reference to constant pool a návratovou adresu. Čím více lokálních proměnných metoda má, tím větší je velikost jejího rámce a tím méně metod lze zavolat před zaplněním zásobníku. Metoda s 10 parametry a 20 lokálními proměnnými zabírá přibližně 3krát více místa než metoda bez parametrů.

Na Androidu ART používá vlastní implementaci zásobníku, odlišnou od Desktop JVM. ART může dynamicky zvětšovat zásobník v určitých mezích, ale pro každé vlákno stále existuje pevný limit. Hlavní vlákno (UI vlákno) má největší zásobník, protože na něm je prováděn celý životní cyklus Activity a zpracování událostí.

kotlin
// Rekurze vedoucí k StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // žádná základní podmínka
}

// Volání povede k StackOverflowError v hloubce ~1000
recursiveCall(0)

Hlavní příčiny přetečení zásobníku

Pět typických scénářů vede k StackOverflowError v mobilních aplikacích. Většina z nich souvisí s rekurzí, ale existují i méně zřejmé příčiny.

Nekonečná rekurze bez základní podmínky

Nejčastější příčina. Vývojář napíše rekurzivní metodu bez podmínky zastavení nebo s podmínkou, která se nikdy nestane true. Každé volání přidává rámec a zásobník se zaplní po 500–2000 iteracích v závislosti na velikosti rámce. Typický příklad: výpočet faktoriálu n! bez kontroly n == 0.

Zkontrolujte základní podmínku na začátku každé rekurzivní metody. V Kotlinu použijte require() nebo check() pro validaci parametrů na startu. Pro hlubokou rekurzi (více než 100 úrovní) zvažte nahrazení iterativním přístupem.

Cyklické závislosti v konstruktorech

Třída A vytváří instanci B, třída B vytváří instanci A — to je cyklická závislost v konstruktorech. Při pokusu o vytvoření A je volán konstruktor B, který volá konstruktor A, a tak dále až do StackOverflowError. DI frameworky (Dagger, Hilt) odhalují takové cykly ve fázi kompilace, ale ruční vytváření objektů je nezachytí.

Používejte Dependency Injection s grafy závislostí: Dagger nebo Koin kontrolují cykly ve fázi sestavení. Pokud je cyklus nevyhnutelný, nahraďte přímou závislost rozhraním s línou inicializací nebo továrnou Provider.

kotlin
// Cyklická závislost — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Líné řešení
class A(private val bProvider: Provider<B>)

Hluboká rekurze při procházení grafů

Procházení stromu View (ViewGroup.getChildAt()), souborového systému nebo JSON struktury pomocí rekurze může překročit limit zásobníku při hloubce větší než 500–1000 prvků. Android ViewGroup s vnořením 20 úrovní je vzácný, ale rekurzivní parsování JSON s 2000 vnořenými objekty je reálný scénář.

Nahraďte rekurzivní procházení iterativním pomocí explicitního Stack<T> nebo ArrayDeque. To zcela eliminuje riziko přetečení zásobníku, protože objekty v haldě nejsou omezeny limitem zásobníku. BFS (Breadth-First Search) přes Queue také řeší problém.

Nesprávné zpracování onConfigurationChanged

Android-specifická příčina: cyklické volání metod životního cyklu při nesprávném zpracování konfigurace. Například v onConfigurationChanged je voláno recreate(), které znovu volá onConfigurationChanged, a tak dále až do StackOverflowError. Podobně: setContentView() uvnitř onLayout(), které způsobuje opakované měření a layout.

Nevolejte recreate() uvnitř metod souvisejících se změnou konfigurace. Pro aktualizaci UI při změně motivu použijte setTheme() bez recreate. Pro dynamickou změnu orientace — requestOrientation() jednou, bez flagu v konfiguraci.

Serializace s cyklickými referencemi

Gson, Moshi nebo Kotlin Serialization při pokusu o serializaci objektu s cyklickými referencemi (A odkazuje na B, B odkazuje na A) upadnou do nekonečné rekurze a spadnou s StackOverflowError. To je častý problém při serializaci Entity s bidirectional Relationship (JPA, Room s ForeignKey).

Použijte @Transient, @JsonIgnore nebo @kotlinx.serialization.Transient pro jednu ze stran cyklu. Pro Gson — JsonSerializer s explicitním omezením hloubky. Pro Room — nikdy neserializujte Entity přímo, používejte DTO mapper.

Jak diagnostikovat a opravit StackOverflowError

Diagnostika StackOverflowError je jednodušší než u jiných chyb paměti: stack trace ve většině případů ukazuje opakující se posloupnost volání. To okamžitě ukazuje na rekurzi.

Čtení stack trace

Stack trace StackOverflowError je jedinečný: po prvních 200–500 řádcích začíná opakování stejného vzoru volání. JVM ořízne opakující se řádky na konci a zobrazí „... 1234 more”. Počet neopakujících se řádků před „...” ukazuje hloubku rekurze, která vedla k chybě.

Přečtěte si první řádky stack trace — ukazují, od které metody začalo opakování. Najděte metodu, která volá sama sebe nebo vytváří řetězec volání vracející se k ní. Opravte základní podmínku nebo nahraďte rekurzi smyčkou.

Zvětšení velikosti zásobníku (dočasné řešení)

Dočasně lze problém vyřešit zvětšením velikosti zásobníku pomocí přepínače JVM -Xss. V Androidu se velikost zásobníku nastavuje přes AndroidManifest: android:largeHeap neovlivňuje zásobník. Pro zvětšení zásobníku vlákna v kódu: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — požadovaná velikost v bajtech.

kotlin
// Vytvoření vlákna se zvětšeným zásobníkem
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

Důležité: zvětšení zásobníku neřeší problém, pouze ho odkládá. Při rekurzi 10 000 úrovní bude zásobník 64 KB nahrazen zásobníkem 128 KB, což poskytne 20 000 úrovní — ale chyba se stejně objeví, jen později. Jediné správné řešení — iterativní náhrada rekurze.

Nahrazení rekurze iterací

Iterativní algoritmy nepoužívají zásobník volání k ukládání mezistavů — ukládají je do haldy (Stack<T> nebo ArrayDeque). Procházení binárního stromu, výpočet faktoriálu, Fibonacci — jakoukoli rekurzi lze převést na iteraci pomocí explicitního zásobníku.

kotlin
// Iterativní procházení stromu — bez rizika 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) }
    }
}

Jak předcházet Stack Overflow

Prevence StackOverflowError je soubor pravidel a nástrojů, které odhalují potenciální rekurzivní cykly dříve, než se dostanou do produkce.

Limit hloubky rekurze v debug sestavení

Přidejte ochranný čítač hloubky do rekurzivních metod v debug sestavení. Pokud hloubka překročí práh (např. 1000), vyhoďte výjimku se srozumitelnou zprávou. To mění StackOverflowError s nečitelným trace na srozumitelnou obchodní výjimku.

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("Rekurze překročila 1000 úrovní")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

Statická analýza kódu

Detekt (Kotlin) a Infer (Facebook) nacházejí potenciálně nekonečné rekurze na úrovni statické analýzy. Detekt má pravidlo PotentiallyInfiniteRecursion, které varuje o self-call bez změny parametrů. Zahrňte ho do sady pravidel CI a nastavte severity na error.

Code Review se zaměřením na rekurzi

Při code review věnujte pozornost: jakýmkoli self-call metodám, rekurzivním voláním uvnitř lambd (inline funkce Kotlin), cyklickým voláním mezi různými třídami, rekurzi v property delegates. U každé rekurzivní metody zkontrolujte: existuje základní podmínka, mění se parametr v každém kroku, zaručuje změna parametru dosažení základní podmínky.

Ocasně-rekurzivní transformace (omezeně)

Kotlin podporuje modifikátor tailrec: pokud je rekurzivní metoda označena tailrec a volání je ocasní (poslední operace), kompilátor ji převede na iteraci. Tailrec však funguje pouze pro self-call (metoda volá přímo sama sebe), nefunguje pro vzájemnou rekurzi a není podporován v Android-kompatibilních verzích Kotlin před 1.5.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // ocasní volání
}

Často kladené otázky

Lze StackOverflowError zachytit pomocí try-catch?

Lze, ale pouze na úrovni Java. Error, stejně jako Exception, je Throwable. Po StackOverflowError je však zásobník poškozen — rámce, které se nevešly, se nemohou správně dokončit. Pokus o vytvoření nového objektu v catch bloku může způsobit další StackOverflowError.

Jaká je výchozí velikost zásobníku v Androidu?

Pro hlavní vlákno — 32–48 KB, pro vlákno na pozadí — 16–24 KB. Přesná velikost závisí na verzi Androidu a výrobci zařízení. ART používá dynamické rozšiřování zásobníku, ale ne více než 2× od počáteční hodnoty.

Může ocasní rekurze zabránit StackOverflowError?

V Kotlinu — ano, pokud je metoda označena tailrec. Kompilátor převede ocasní rekurzi na iteraci, zcela eliminující růst zásobníku. V Javě není ocasní rekurze optimalizována JVM (na rozdíl od funkcionálních jazyků jako Scala).

Proč se StackOverflowError vyskytuje na emulátoru, ale ne na zařízení?

Velikost zásobníku na emulátoru a skutečném zařízení se může lišit. Emulátor používá Desktop JVM s typickým zásobníkem 512–1024 KB, zatímco Android ART — 32–48 KB. Chyba se na ART objeví dříve než na Desktop JVM.

Čím se StackOverflowError liší od OutOfMemoryError?

Oblast paměti: StackOverflowError — chyba zásobníku (rámce volání), OutOfMemoryError — chyba haldy (objekty). StackOverflowError je téměř vždy způsoben rekurzí, zatímco OutOfMemoryError — úniky paměti nebo velkými objekty.

Shrnutí

  • StackOverflowError — přetečení zásobníku volání při překročení limitu hloubky rekurze
  • Hloubka zásobníku v Androidu je 512–1024 rámců na hlavním vlákně
  • Nekonečná rekurze — hlavní příčina; zkontrolujte základní podmínku v každé rekurzivní metodě
  • Cyklické závislosti v konstruktorech — méně zřejmá, ale častá příčina přetečení
  • Iterativní náhrada rekurze pomocí explicitního Stack<T> zcela eliminuje riziko
  • tailrec v Kotlinu převádí ocasní rekurzi na iteraci na úrovni kompilátoru
  • Statická analýza (Detekt, Infer) nachází potenciálně nekonečné rekurze před spuštěním

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také