Mobil inkişafda Stack Overflow — bu nədir, səbəbləri və qarşısını alma üsulları

Müəllif: IT Sectr Dərc olunub: 2026-03-29 Oxuma vaxtı: 9 dəq

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 — zəng yığınının dərinlik limiti aşıldıqda JVM səhvi
  • Yığın dərinliyi məhduddur və konfiqurasiyadan asılı olaraq 512–2048 freym təşkil edir
  • Sonsuz rekursiya — StackOverflowError-un ən çox yayılmış səbəbi
  • Quyruq rekursiyası funksional dillərdən fərqli olaraq JVM-də optimallaşdırılmır
  • İterativ əvəzetmə rekursiyanın — daşqının qarşısını almaq üçün etibarlı üsul

Stack Overflow nədir

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ı necə işləyir

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.

kotlin
// 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)

Yığın daşqınının əsas səbəbləri

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.

Əsas dayanma şərti olmayan sonsuz rekursiya

Ə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.

Konstruktorlarda tsiklik asılılıqlar

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.

kotlin
// 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>)

Qrafların keçidində dərin rekursiya

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.

onConfigurationChanged-in səhv işlənməsi

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().

Tsiklik istinadlarla serializasiya

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-u necə diaqnoz etmək və düzəltmək

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.

Stack trace-in oxunması

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.

Yığın ölçüsünün artırılması (müvəqqəti həll)

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çü.

kotlin
// 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.

Rekursiyanın iterasiya ilə əvəz edilməsi

İ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.

kotlin
// 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) }
    }
}

Stack Overflow-un qarşısını necə almaq

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 rekursiya dərinliyi limiti

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.

kotlin
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)
}

Statik kod analizi

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.

Rekursiyaya fokuslanmış Code Review

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.

Quyruq-rekursiv transformasiya (məhdud)

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.

kotlin
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

StackOverflowError-u try-catch ilə tutmaq olarmı?

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.

Android-də standart yığın ölçüsü nə qədərdir?

Ə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.

Quyruq rekursiyası StackOverflowError-un qarşısını ala bilərmi?

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).

Niyə StackOverflowError emulatorda baş verir, amma cihazda yox?

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.

StackOverflowError OutOfMemoryError-dan nə ilə fərqlənir?

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ə

  • StackOverflowError — rekursiya dərinliyi limiti aşıldıqda zəng yığınının daşması
  • Yığın dərinliyi Android-də əsas iplikdə 512–1024 freym təşkil edir
  • Sonsuz rekursiya — əsas səbəb; hər rekursiv metoddə əsas şərti yoxlayın
  • Konstruktorlarda tsiklik asılılıqlar — daha az aşkar, lakin tez-tez rast gəlinən daşqın səbəbi
  • Rekursiyanın iterativ əvəz edilməsi aşkar Stack<T> vasitəsilə riski tamamilə aradan qaldırır
  • tailrec Kotlin-də quyruq rekursiyasını kompilyator səviyyəsində iterasiyaya çevirir
  • Statik analiz (Detekt, Infer) potensial sonsuz rekursiyaları icradan əvvəl tapır

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.

Layihəni müzakirə et

Həm də oxuyun