Stack Overflow — zəng yığınının daşması səhvi (java.lang.StackOverflowError), iplik yığınının maksimal dərinliyi aşıldıqda baş verir. Java Virtual Machine Specification-a görə, JVM-də tipik yığın dərinliyi 64-bit sistemlər üçün 1024 freymdir. Əsas səbəb — əsas dayanma şərti olmayan sonsuz rekursiyadır.
Əsas məqamlar
StackOverflowError — bu Java Virtual Machine (JVM) və ya Android Runtime (ART) ölümcül səhvidir, iplik zəng yığını maksimal icazə verilən dərinliyə çatdıqda baş verir. OutOfMemoryError-dan (Heap çatışmazlığı) fərqli olaraq, StackOverflowError yaddaşın digər sahəsi — metod çağırış freymlərinin və lokal dəyişənlərin saxlandığı yığınla bağlıdır.
Hər bir metod çağırışı yığında freym yaradır: qayıdış ünvanı, parametrlər və lokal dəyişənlər. Metoddan qayıdışda freym məhv edilir. Əgər metod özünü çağırırsa (rekursiya) əsas şərt olmadan, freymlər yığın dolana qədər yığılır. JVM yeni freym ayıra bilmir və «null» mesajı ilə (Java-da) və ya sonsuz təkrarlanan yığın sətri göstərilməklə StackOverflowError atır.
İplik yığınının ölçüsü yaradılarkən fiksasiya olunur və icra zamanı dəyişmir. Android-də əsas iplik yığınının tipik ölçüsü 32–48 KB-dir, bu da çox sayda lokal dəyişəni olmayan metodlar üçün təxminən 512–1024 freym dərinliyi verir. Fon iplikləri üçün standart ölçü daha kiçikdir — 16–24 KB.
Zəng yığını (Call Stack) — metodların icra sırasını idarə edən LIFO (Last In, First Out) məlumat strukturudur. Proqram metodu çağırdıqda, JVM yığında freym yaradır və onu yuxarıya yerləşdirir. Metod tamamlandıqda freym çıxarılır.
Hər freym ehtiva edir: operand stack (baytkod təlimatları üçün operand yığını), array of local variables (this daxil olmaqla), reference to constant pool və qayıdış ünvanı. Metodun nə qədər çox lokal dəyişəni varsa, freyminin ölçüsü bir o qədər böyük olur və yığın dolana qədər bir o qədər az metod çağırıla bilər. 10 parametrli və 20 lokal dəyişənli metod parametrsiz metoddan təxminən 3 dəfə çox yer tutur.
Android-də ART Desktop JVM-dən fərqli olaraq öz yığın tətbiqini istifadə edir. ART müəyyən həddə qədər yığını dinamik olaraq artıra bilər, lakin hər iplik üçün yenə də sərt limit mövcuddur. Əsas iplik (UI-iplik) ən böyük yığına malikdir, çünki Activity-nin bütün həyat dövrü və hadisələrin işlənməsi onun üzərində icra olunur.
// StackOverflowError-a səbəb olan rekursiya
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // əsas şərt yoxdur
}
// Çağırış ~1000 dərinliyində StackOverflowError-a səbəb olacaq
recursiveCall(0)
Beş tipik ssenari mobil tətbiqlərdə StackOverflowError-a gətirib çıxarır. Onların əksəriyyəti rekursiya ilə bağlıdır, lakin daha az aşkar səbəblər də var.
Ən çox yayılmış səbəb. Proqramçı dayanma şərti olmayan və ya heç vaxt true olmayan şərtlə rekursiv metod yazır. Hər çağırış freym əlavə edir və yığın freym ölçüsündən asılı olaraq 500–2000 iterasiyadan sonra dolur. Tipik nümunə: n == 0 yoxlamadan n! faktorialının hesablanması.
Hər rekursiv metodun əvvəlində əsas şərti yoxlayın. Kotlin-də parametrləri başlanğıcda yoxlamaq üçün require() və ya check() istifadə edin. Dərin rekursiya üçün (100 səviyyədən çox) iterativ yanaşma ilə əvəz etməyi düşünün.
A sinfi B nümunəsini yaradır, B sinfi A nümunəsini yaradır — bu konstruktorlarda tsiklik asılılıqdır. A-nı yaratmağa cəhd etdikdə B-nin konstruktoru çağırılır, B A-nın konstruktorunu çağırır və bu StackOverflowError-a qədər davam edir. DI freymvorkları (Dagger, Hilt) belə dövrləri kompilyasiya mərhələsində aşkarlayır, lakin əl ilə obyekt yaratma onları tutmur.
Asılılıq qrafları ilə Dependency Injection istifadə edin: Dagger və ya Koin dövrləri qurma mərhələsində yoxlayır. Əgər dövr qaçılmazdırsa, birbaşa asılılığı gecikmiş inisializasiyalı interfeys və ya Provider fabriki ilə əvəz edin.
// Tsiklik asılılıq — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Gecikmiş həll
class A(private val bProvider: Provider<B>)
View ağacının (ViewGroup.getChildAt()), fayl sisteminin və ya JSON strukturunun rekursiya vasitəsilə keçidi 500–1000 elementdən çox dərinlikdə yığın limitini aşa bilər. 20 səviyyəli iç-içə Android ViewGroup nadir hallarda olur, lakin 2000 iç-içə obyekti olan JSON-un rekursiv parsingsi real ssenaridir.
Rekursiv keçidi aşkar Stack<T> və ya ArrayDeque vasitəsilə iterativ ilə əvəz edin. Bu, yığın daşqını riskini tamamilə aradan qaldırır, çünki yığındakı obyektlər yığın limiti ilə məhdudlaşmır. Queue vasitəsilə BFS (Breadth-First Search) də problemi həll edir.
Android-spesifik səbəb: konfiqurasiyanın səhv işlənməsi zamanı həyat dövrü metodlarının tsiklik çağırışı. Məsələn, onConfigurationChanged daxilində recreate() çağırılır, o da yenidən onConfigurationChanged-i çağırır və bu StackOverflowError-a qədər davam edir. Eynilə: onLayout() daxilində təkrar ölçü və layout-a səbəb olan setContentView().
Konfiqurasiya dəyişikliyi ilə bağlı metodlar daxilində recreate() çağırmayın. Tema dəyişikliyi zamanı UI yeniləmək üçün recreate olmadan setTheme() istifadə edin. Dinamik oriyentasiya dəyişikliyi üçün — konfiqurasiyada flag olmadan, bir dəfəlik requestOrientation().
Gson, Moshi və ya Kotlin Serialization tsiklik istinadları olan obyekti (A B-yə istinad edir, B A-ya istinad edir) serializasiya etməyə cəhd etdikdə sonsuz rekursiyaya gedir və StackOverflowError ilə çökür. Bu, bidirectional Relationship (JPA, ForeignKey ilə Room) ilə Entity serializasiyasında tez-tez rast gəlinən problemdir.
Dövrün tərəflərindən biri üçün @Transient, @JsonIgnore və ya @kotlinx.serialization.Transient istifadə edin. Gson üçün — aşkar dərinlik məhdudiyyəti ilə JsonSerializer. Room üçün — Entity-ni heç vaxt birbaşa serializasiya etməyin, DTO mapper-lərdən istifadə edin.
StackOverflowError-un diaqnostikası digər yaddaş səhvlərindən daha asandır: stack trace əksər hallarda təkrarlanan çağırış ardıcıllığını göstərir. Bu, dərhal rekursiyaya işarə edir.
StackOverflowError-un stack trace-i unikaldır: ilk 200–500 sətirdən sonra eyni çağırış nümunəsinin təkrarlanması başlayır. JVM sonda təkrarlanan sətirləri kəsir və «... 1234 more» göstərir. «...» qarşısındakı təkrarlanmayan sətirlərin sayı səhvə səbəb olan rekursiyanın dərinliyini göstərir.
Stack trace-in ilk sətirlərini oxuyun — onlar təkrarlamanın hansı metoddan başladığını göstərir. Özünü çağıran və ya ona qayıdan çağırış zənciri yaradan metodu tapın. Əsas şərti düzəldin və ya rekursiyanı dövrlə əvəz edin.
Müvəqqəti olaraq problemi JVM -Xss flagı vasitəsilə yığın ölçüsünü artırmaqla həll etmək olar. Android-də yığın ölçüsü AndroidManifest vasitəsilə təyin olunur: android:largeHeap yığına təsir etmir. Kodda iplik yığınını artırmaq üçün: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — baytlarla istənilən ölçü.
// Artırılmış yığınla iplik yaratma
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Vacib: yığının artırılması problemi həll etmir, sadəcə onu təxirə salır. 10 000 səviyyəli rekursiyada 64 KB yığın 128 KB yığınla əvəz olunacaq, bu da 20 000 səviyyə verəcək — lakin səhv yenə də baş verəcək, sadəcə gec. Yeganə düzgün həll — rekursiyanın iterativ əvəz edilməsidir.
İterativ alqoritmlər aralıq vəziyyətləri saxlamaq üçün zəng yığınından istifadə etmir — onları yığında saxlayır (Stack<T> və ya ArrayDeque). İkili ağacın keçidi, faktorialın hesablanması, Fibonaççi — istənilən rekursiya aşkar yığın vasitəsilə iterasiyaya çevrilə bilər.
// Ağacın iterativ keçidi — StackOverflow riski olmadan
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-un profilaktikası — potensial rekursiv dövrləri istehsala düşməzdən əvvəl aşkarlayan qaydalar və alətlər toplusudur.
Debug yığımında rekursiv metodlara qoruyucu dərinlik sayğacı əlavə edin. Əgər dərinlik həddi aşarsa (məsələn, 1000), aydın mesajla istisna atın. Bu, oxunmayan trace ilə StackOverflowError-u anlaşılan biznes istisnasına çevirir.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("Rekursiya 1000 səviyyəni keçdi")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) və Infer (Facebook) statik analiz səviyyəsində potensial sonsuz rekursiyaları tapır. Detekt-də parametrləri dəyişmədən self-call barədə xəbərdarlıq edən PotentiallyInfiniteRecursion qaydası var. Onu CI qaydalar dəstinə daxil edin və severity-ni error-a təyin edin.
Code review-da diqqət yetirin: hər hansı self-call metodlara, lambda daxilində rekursiv çağırışlara (Kotlin inline-funksiyaları), müxtəlif siniflər arasında tsiklik çağırışlara, property delegates-də rekursiyaya. Hər rekursiv metod üçün yoxlayın: əsas şərt varmı, parametr hər addımda dəyişirmi, parametrin dəyişməsi əsas şərtə çatmağı təmin edirmi.
Kotlin tailrec modifierini dəstəkləyir: əgər rekursiv metod tailrec ilə işarələnibsə və çağırış quyruqdursa (son əməliyyat), kompilyator onu iterasiyaya çevirir. Lakin tailrec yalnız self-call üçün işləyir (metod birbaşa özünü çağırır), qarşılıqlı rekursiya üçün işləmir və Android-ə uyğun Kotlin versiyalarında 1.5-ə qədər dəstəklənmir.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // quyruq çağırışı
}
Tez-tez verilən suallar
Bəli, ancaq yalnız Java səviyyəsində. Error, Exception kimi, Throwable-dir. Lakin StackOverflowError-dan sonra yığın zədələnib — sığmayan freymlər düzgün tamamlana bilmir. Catch blokunda yeni obyekt yaratmaq cəhdi təkrar StackOverflowError-a səbəb ola bilər.
Əsas iplik üçün — 32–48 KB, fon ipliyi üçün — 16–24 KB. Dəqiq ölçü Android versiyasından və cihaz istehsalçısından asılıdır. ART dinamik yığın genişlənməsindən istifadə edir, lakin başlanğıc dəyərin 2×-dən çox deyil.
Kotlin-də — bəli, əgər metod tailrec ilə işarələnibsə. Kompilyator quyruq rekursiyasını iterasiyaya çevirir, yığın artımını tamamilə aradan qaldırır. Java-da quyruq rekursiyası JVM tərəfindən optimallaşdırılmır (Scala kimi funksional dillərdən fərqli olaraq).
Yığın ölçüsü emulatorda və real cihazda fərqlənə bilər. Emulator tipik yığını 512–1024 KB olan Desktop JVM-dən istifadə edir, Android ART isə 32–48 KB. Səhv ART-də Desktop JVM-dən daha tez baş verəcək.
Yaddaş sahəsi: StackOverflowError — yığın səhvi (çağırış freymləri), OutOfMemoryError — yığın səhvi (obyektlər). StackOverflowError demək olar ki, həmişə rekursiya nəticəsində yaranır, OutOfMemoryError isə yaddaş sızmaları və ya böyük obyektlər nəticəsində.
Xülasə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun