Stack Overflow — грешка прекорачења стека позива (java.lang.StackOverflowError), која настаје при прекорачењу максималне дубине стека нити. Према Java Virtual Machine Specification, типична дубина стека у JVM износи 1024 оквира за 64-битне системе. Основни узрок — бесконачна рекурзија без базног услова заустављања.
Главне тачке
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-ја и обрада догађаја.
// Рекурзија која доводи до 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 фабриком.
// Циклична зависност — 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 такође решава проблем.
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-а је једноставнија од других грешака меморије: 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 — жељена величина у бајтовима.
// Креирање нити са увећаним стеком
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Важно: повећање стека не решава проблем, већ га само одлаже. При рекурзији од 10 000 нивоа, стек од 64 KB замениће се стеком од 128 KB, што ће дати 20 000 нивоа — али грешка ће се ипак догодити, само касније. Једино исправно решење — итеративна замена рекурзије.
Итеративни алгоритми не користе стек позива за чување међустања — чувају их у хеапу (Stack<T> или ArrayDeque). Обилазак бинарног стабла, израчунавање факторијела, Фибоначи — било коју рекурзију може се претворити у итерацију кроз експлицитни стек.
// Итеративни обилазак стабла — без ризика од 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) }
}
}
Превенција StackOverflowError-а је скуп правила и алата који откривају потенцијалне рекурзивне циклусе пре него што доспеју у продукцију.
Додајте заштитни бројач дубине у рекурзивне методе у debug верзији. Ако дубина прелази праг (нпр. 1000), бацајте изузетак са разумљивом поруком. Ово претвара StackOverflowError са нечитљивим trace-ом у разумљив пословни изузетак.
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 обратите пажњу на: све self-call методе, рекурзивне позиве унутар ламбди (inline функције Kotlin-а), цикличне позиве између различитих класа, рекурзију у property delegates. За сваки рекурзивни метод проверите: да ли постоји базни услов, да ли се параметар мења на сваком кораку, да ли промена параметра гарантује достизање базног услова.
Kotlin подржава tailrec модификатор: ако је рекурзивни метод означен са tailrec и позив је репутациони (последња операција), компајлер га претвара у итерацију. Међутим, tailrec ради само за self-call (метод директно позива сам себе), не ради за узајамну рекурзију и није подржан у Android-компатибилним верзијама Kotlin-а пре 1.5.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // репутациони позив
}
Често постављана питања
Може, али само на Java нивоу. Error, као и Exception, је Throwable. Међутим, после StackOverflowError-а стек је оштећен — оквири који нису стали не могу се коректно завршити. Покушај креирања новог објекта у catch блоку може изазвати поновни StackOverflowError.
За главну нит — 32–48 KB, за позадинску — 16–24 KB. Тачна величина зависи од верзије Android-а и произвођача уређаја. ART користи динамичко проширење стека, али не више од 2× од почетне вредности.
У Kotlin-у — да, ако је метод означен са tailrec. Компајлер претвара репутациону рекурзију у итерацију, потпуно елиминишући раст стека. У Java, репутациона рекурзија се не оптимизује у JVM (за разлику од функционалних језика попут Scala).
Величина стека на емулатору и стварном уређају може се разликовати. Емулатор користи Desktop JVM са типичним стеком од 512–1024 KB, а Android ART — 32–48 KB. Грешка ће се појавити на ART раније него на Desktop JVM.
Област меморије: StackOverflowError — грешка стека (оквири позива), OutOfMemoryError — грешка хеапа (објекти). StackOverflowError је готово увек изазван рекурзијом, а OutOfMemoryError — цурењем меморије или великим објектима.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође