Stack Overflow у мобилном развоју — шта је то, узроци и методе превенције

Аутор: IT Sectr Објављено: 2026-03-29 Време читања: 9 мин

Stack Overflow — грешка прекорачења стека позива (java.lang.StackOverflowError), која настаје при прекорачењу максималне дубине стека нити. Према Java Virtual Machine Specification, типична дубина стека у JVM износи 1024 оквира за 64-битне системе. Основни узрок — бесконачна рекурзија без базног услова заустављања.

Главне тачке

  • StackOverflowError — грешка JVM при прекорачењу лимита дубине стека позива
  • Дубина стека је ограничена и износи 512–2048 оквира у зависности од конфигурације
  • Бесконачна рекурзија — најчешћи узрок StackOverflowError
  • Репутациона рекурзија се не оптимизује у JVM, за разлику од функционалних језика
  • Итеративна замена рекурзије — поуздан начин спречавања прекорачења

Шта је Stack Overflow

StackOverflowError — је фатална грешка виртуелне машине Java (JVM) или Android Runtime (ART), која настаје када стек позива нити достигне максималну дозвољену дубину. За разлику од OutOfMemoryError (недостатак Heap-а), StackOverflowError је повезан са другом облашћу меморије — стеком, где се чувају оквири позива метода и локалне променљиве.

Сваки позив метода креира оквир у стеку: адресу повратка, параметре и локалне променљиве. При повратку из метода, оквир се уништава. Ако метод позива сам себе (рекурзија) без базног услова, оквири се акумулирају до попуњавања стека. JVM не може да издвоји нови оквир и баца StackOverflowError са поруком „null“ (у Java) или са навођењем бесконачно понављајућег низа стека.

Величина стека нити је фиксирана при креирању и не мења се током извршавања. У Android-у типична величина стека главне нити је 32–48 KB, што даје дубину од приближно 512–1024 оквира за методе без великог броја локалних променљивих. За позадинске нити, подразумевана величина је мања — 16–24 KB.

Како је уређен стек позива

Стек позива (Call Stack) — је структура података LIFO (Last In, First Out) која управља редоследом извршавања метода. Сваки пут када програм позове метод, JVM креира оквир у стеку и поставља га на врх. При завршетку метода, оквир се уклања.

Сваки оквир садржи: operand stack (стек операнда за бајткод инструкције), array of local variables (укључујући this), reference to constant pool и адресу повратка. Што више локалних променљивих метод има, већа је величина његовог оквира и мање метода се може позвати пре попуњавања стека. Метод са 10 параметара и 20 локалних променљивих заузима приближно 3 пута више простора од метода без параметара.

На Android-у ART користи сопствену имплементацију стека, различиту од Desktop JVM. ART може динамички да повећава стек у одређеним границама, али за сваку нит и даље постоји чврсти лимит. Главна нит (UI нит) има највећи стек, јер се на њој извршава цео животни циклус Activity-ја и обрада догађаја.

kotlin
// Рекурзија која доводи до StackOverflowError
fun recursiveCall(depth: Int): Int {
    return recursiveCall(depth + 1) // нема базног услова
}

// Позив ће довести до StackOverflowError на дубини ~1000
recursiveCall(0)

Основни узроци прекорачења стека

Пет типичних сценарија доводи до StackOverflowError у мобилним апликацијама. Већина њих је повезана са рекурзијом, али постоје и мање очигледни узроци.

Бесконачна рекурзија без базног услова

Најчешћи узрок. Програмер пише рекурзивни метод без услова заустављања или са условом који никада не постаје true. Сваки позив додаје оквир, а стек се пуни после 500–2000 итерација у зависности од величине оквира. Типичан пример: израчунавање факторијела n! без провере n == 0.

Проверите базни услов на почетку сваког рекурзивног метода. У Kotlin-у користите require() или check() за валидацију параметара на старту. За дубоку рекурзију (више од 100 нивоа) размотрите замену итеративним приступом.

Цикличне зависности у конструкторима

Класа A креира инстанцу B, класа B креира инстанцу A — ово је циклична зависност у конструкторима. При покушају креирања A позива се конструктор B, који позива конструктор A и тако до StackOverflowError. DI оквири (Dagger, Hilt) откривају такве циклусе у фази компајлирања, али ручно креирање објеката их не хвата.

Користите Dependency Injection са графовима зависности: Dagger или Koin проверавају циклусе у фази изградње. Ако је циклус неизбежан, замените директну зависност интерфејсом са лењом иницијализацијом или Provider фабриком.

kotlin
// Циклична зависност — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Лењи приступ
class A(private val bProvider: Provider<B>)

Дубока рекурзија при обиласку графова

Обилазак стабла View (ViewGroup.getChildAt()), датотечног система или JSON структуре кроз рекурзију може премашити лимит стека при дубини већој од 500–1000 елемената. Android ViewGroup са угњежденошћу од 20 нивоа је ретка, али рекурзивно парсирање JSON-а са 2000 угњеждених објеката је реалан сценарио.

Замијените рекурзивни обилазак итеративним кроз експлицитни Stack<T> или ArrayDeque. Ово потпуно елиминише ризик од прекорачења стека, јер објекти у хеапу нису ограничени лимитом стека. BFS (Breadth-First Search) кроз Queue такође решава проблем.

Неправилна обрада onConfigurationChanged

Android-специфичан узрок: циклично позивање метода животног циклуса при неправилној обради конфигурације. На пример, у onConfigurationChanged се позива recreate(), који поново позива onConfigurationChanged и тако до StackOverflowError. Слично: setContentView() унутар onLayout(), који изазива поновно мерење и layout.

Не позивајте recreate() унутар метода повезаних са променом конфигурације. За ажурирање UI-ја при промени теме користите setTheme() без recreate. За динамичку промену оријентације — requestOrientation() једнократно, без флага у конфигурацији.

Серијализација са цикличним референцама

Gson, Moshi или Kotlin Serialization при покушају серијализације објекта са цикличним референцама (A се односи на B, B се односи на A) одлазе у бесконачну рекурзију и падају са StackOverflowError. Ово је чест проблем при серијализацији Entity-ја са bidirectional Relationship (JPA, Room са ForeignKey).

Користите @Transient, @JsonIgnore или @kotlinx.serialization.Transient за једну од страна циклуса. За Gson — JsonSerializer са експлицитним ограничавањем дубине. За Room — никада не серијализујте Entity директно, користите DTO мапере.

Како дијагностиковати и поправити StackOverflowError

Дијагностика StackOverflowError-а је једноставнија од других грешака меморије: stack trace у већини случајева показује понављајући след позива. Ово одмах указује на рекурзију.

Читање stack trace-а

Stack trace StackOverflowError-а је јединствен: после првих 200–500 линија почиње понављање истог обрасца позива. JVM скраћује понављајуће линије на крају и приказује „... 1234 more“. Број непонављајућих линија пре „...“ показује дубину рекурзије која је довела до грешке.

Прочитајте прве линије stack trace-а — one показују од ког метода је почело понављање. Пронађите метод који позива сам себе или ствара ланац позива који се враћа ка њему. Исправите базни услов или замените рекурзију петљом.

Повећање величине стека (привремено решење)

Привремено се проблем може решити повећањем величине стека кроз флаг JVM -Xss. У Android-у величина стека се подешава кроз AndroidManifest: android:largeHeap не утиче на стек. За повећање стека нити у коду: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — жељена величина у бајтовима.

kotlin
// Креирање нити са увећаним стеком
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()

Важно: повећање стека не решава проблем, већ га само одлаже. При рекурзији од 10 000 нивоа, стек од 64 KB замениће се стеком од 128 KB, што ће дати 20 000 нивоа — али грешка ће се ипак догодити, само касније. Једино исправно решење — итеративна замена рекурзије.

Замена рекурзије итерацијом

Итеративни алгоритми не користе стек позива за чување међустања — чувају их у хеапу (Stack<T> или ArrayDeque). Обилазак бинарног стабла, израчунавање факторијела, Фибоначи — било коју рекурзију може се претворити у итерацију кроз експлицитни стек.

kotlin
// Итеративни обилазак стабла — без ризика од 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) }
    }
}

Како спречити Stack Overflow

Превенција StackOverflowError-а је скуп правила и алата који откривају потенцијалне рекурзивне циклусе пре него што доспеју у продукцију.

Лимит дубине рекурзије у debug верзији

Додајте заштитни бројач дубине у рекурзивне методе у debug верзији. Ако дубина прелази праг (нпр. 1000), бацајте изузетак са разумљивом поруком. Ово претвара StackOverflowError са нечитљивим trace-ом у разумљив пословни изузетак.

kotlin
fun safeRecursive(n: Int, depth: Int = 0): Int {
    if (depth > 1000) {
        throw IllegalStateException("Рекурзија је премашила 1000 нивоа")
    }
    return if (n <= 1) n
           else safeRecursive(n - 1, depth + 1)
}

Статичка анализа кода

Detekt (Kotlin) и Infer (Facebook) проналазе потенцијално бесконачне рекурзије на нивоу статичке анализе. Detekt има правило PotentiallyInfiniteRecursion које упозорава о self-call без промене параметара. Укључите га у скуп правила CI-ја и подесите severity на error.

Code Review са фокусом на рекурзију

На code review обратите пажњу на: све self-call методе, рекурзивне позиве унутар ламбди (inline функције Kotlin-а), цикличне позиве између различитих класа, рекурзију у property delegates. За сваки рекурзивни метод проверите: да ли постоји базни услов, да ли се параметар мења на сваком кораку, да ли промена параметра гарантује достизање базног услова.

Репутациона трансформација (ограничено)

Kotlin подржава tailrec модификатор: ако је рекурзивни метод означен са tailrec и позив је репутациони (последња операција), компајлер га претвара у итерацију. Међутим, tailrec ради само за self-call (метод директно позива сам себе), не ради за узајамну рекурзију и није подржан у Android-компатибилним верзијама Kotlin-а пре 1.5.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // репутациони позив
}

Често постављана питања

Да ли се StackOverflowError може ухватити кроз try-catch?

Може, али само на Java нивоу. Error, као и Exception, је Throwable. Међутим, после StackOverflowError-а стек је оштећен — оквири који нису стали не могу се коректно завршити. Покушај креирања новог објекта у catch блоку може изазвати поновни StackOverflowError.

Која је подразумевана величина стека у Android-у?

За главну нит — 32–48 KB, за позадинску — 16–24 KB. Тачна величина зависи од верзије Android-а и произвођача уређаја. ART користи динамичко проширење стека, али не више од 2× од почетне вредности.

Може ли репутациона рекурзија спречити StackOverflowError?

У Kotlin-у — да, ако је метод означен са tailrec. Компајлер претвара репутациону рекурзију у итерацију, потпуно елиминишући раст стека. У Java, репутациона рекурзија се не оптимизује у JVM (за разлику од функционалних језика попут Scala).

Зашто се StackOverflowError јавља на емулатору, али не и на уређају?

Величина стека на емулатору и стварном уређају може се разликовати. Емулатор користи Desktop JVM са типичним стеком од 512–1024 KB, а Android ART — 32–48 KB. Грешка ће се појавити на ART раније него на Desktop JVM.

По чему се StackOverflowError разликује од OutOfMemoryError?

Област меморије: StackOverflowError — грешка стека (оквири позива), OutOfMemoryError — грешка хеапа (објекти). StackOverflowError је готово увек изазван рекурзијом, а OutOfMemoryError — цурењем меморије или великим објектима.

Резиме

  • StackOverflowError — прекорачење стека позива при прекорачењу лимита дубине рекурзије
  • Дубина стека у Android-у износи 512–1024 оквира на главној нити
  • Бесконачна рекурзија — главни узрок; проверите базни услов у сваком рекурзивном методу
  • Цикличне зависности у конструкторима — мање очигледан, али чест узрок прекорачења
  • Итеративна замена рекурзије кроз експлицитни Stack<T> потпуно елиминише ризик
  • tailrec у Kotlin-у претвара репутациону рекурзију у итерацију на нивоу компајлера
  • Статичка анализа (Detekt, Infer) проналази потенцијално бесконачне рекурзије пре покретања

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође