Stack Overflow در توسعه موبایل — چیست، علل و روش‌های پیشگیری

نویسنده: IT Sectr منتشر شده: 2026-03-29 زمان مطالعه: 9 دقیقه

Stack Overflow — خطای سرریز پشته فراخوانی (java.lang.StackOverflowError) که هنگام تجاوز از حداکثر عمق پشته رشته رخ می‌دهد. بر اساس Java Virtual Machine Specification، عمق معمول پشته در JVM برای سیستم‌های 64 بیتی 1024 فریم است. علت اصلی — بازگشت بی‌نهایت بدون شرط پایه توقف.

نکات کلیدی

  • StackOverflowError — خطای JVM هنگام تجاوز از حد عمق پشته فراخوانی
  • عمق پشته محدود است و بسته به پیکربندی 512–2048 فریم است
  • بازگشت بی‌نهایت — شایع‌ترین علت StackOverflowError
  • بازگشت دم بر خلاف زبان‌های تابعی در JVM بهینه‌سازی نمی‌شود
  • جایگزینی تکراری بازگشت — روشی مطمئن برای جلوگیری از سرریز

Stack Overflow چیست

StackOverflowError — یک خطای مرگبار ماشین مجازی جاوا (JVM) یا Android Runtime (ART) است که زمانی رخ می‌دهد که پشته فراخوانی رشته به حداکثر عمق مجاز می‌رسد. بر خلاف OutOfMemoryError (کمبود Heap)، StackOverflowError با ناحیه دیگری از حافظه — پشته — مرتبط است، جایی که فریم‌های فراخوانی متدها و متغیرهای محلی ذخیره می‌شوند.

هر فراخوانی متد یک فریم در پشته ایجاد می‌کند: آدرس بازگشت، پارامترها و متغیرهای محلی. پس از بازگشت از متد، فریم تخریب می‌شود. اگر متد خودش را فراخوانی کند (بازگشت) بدون شرط پایه، فریم‌ها تا پر شدن پشته انباشته می‌شوند. JVM نمی‌تواند فریم جدیدی اختصاص دهد و StackOverflowError را با پیام «null» (در جاوا) یا با نشان دادن رشته پشته بی‌نهایت تکراری پرتاب می‌کند.

اندازه پشته رشته در زمان ایجاد ثابت است و در طول اجرا تغییر نمی‌کند. در Android اندازه معمول پشته رشته اصلی 32–48 کیلوبایت است که عمق تقریباً 512–1024 فریم را برای متدهایی بدون تعداد زیادی متغیر محلی فراهم می‌کند. برای رشته‌های پس‌زمینه، اندازه پیش‌فرض کوچک‌تر است — 16–24 کیلوبایت.

پشته فراخوانی چگونه کار می‌کند

پشته فراخوانی (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) // شرط پایه وجود ندارد
}

// فراخوانی در عمق ~1000 به StackOverflowError منجر می‌شود
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() یک بار، بدون flag در پیکربندی.

سریال‌سازی با ارجاعات چرخه‌ای

Gson، Moshi یا Kotlin Serialization هنگام تلاش برای سریال‌سازی یک شیء با ارجاعات چرخه‌ای (A به B ارجاع می‌دهد، B به A ارجاع می‌دهد) وارد بازگشت بی‌نهایت می‌شوند و با StackOverflowError سقوط می‌کنند. این یک مشکل رایج در سریال‌سازی Entity با bidirectional Relationship (JPA، Room با ForeignKey) است.

برای یکی از طرف‌های چرخه از @Transient، @JsonIgnore یا @kotlinx.serialization.Transient استفاده کنید. برای Gson — JsonSerializer با محدودیت عمق صریح. برای Room — هرگز Entity را مستقیماً سریال‌سازی نکنید، از mapperهای DTO استفاده کنید.

نحوه تشخیص و رفع StackOverflowError

تشخیص StackOverflowError ساده‌تر از سایر خطاهای حافظه است: stack trace در بیشتر موارد یک دنباله تکراری از فراخوانی‌ها را نشان می‌دهد. این بلافاصله به بازگشت اشاره می‌کند.

خواندن stack trace

Stack trace StackOverflowError منحصربه‌فرد است: پس از 200–500 خط اول، تکرار همان الگوی فراخوانی شروع می‌شود. JVM خطوط تکراری را در انتها قطع می‌کند و «... 1234 more» را نشان می‌دهد. تعداد خطوط غیر تکراری قبل از «...» عمق بازگشتی را نشان می‌دهد که منجر به خطا شده است.

خطوط اول stack trace را بخوانید — آنها نشان می‌دهند که تکرار از کدام متد شروع شده است. متدی را پیدا کنید که خودش را فراخوانی می‌کند یا زنجیره فراخوانی ایجاد می‌کند که به آن بازمی‌گردد. شرط پایه را اصلاح کنید یا بازگشت را با حلقه جایگزین کنید.

افزایش اندازه پشته (راه‌حل موقت)

به طور موقت می‌توان مشکل را با افزایش اندازه پشته از طریق flag 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 کیلوبایتی با پشته 128 کیلوبایتی جایگزین می‌شود که 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 کار می‌کند (متد مستقیماً خودش را فراخوانی می‌کند)، برای بازگشت متقابل کار نمی‌کند و در نسخه‌های Kotlin سازگار با Android قبل از 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 گرفت؟

می‌توان، اما فقط در سطح جاوا. Error مانند Exception، Throwable است. با این حال پس از StackOverflowError پشته آسیب دیده است — فریم‌هایی که جا نشده‌اند نمی‌توانند به درستی کامل شوند. تلاش برای ایجاد یک شیء جدید در بلوک catch ممکن است StackOverflowError دیگری ایجاد کند.

اندازه پیش‌فرض پشته در Android چقدر است؟

برای رشته اصلی — 32–48 کیلوبایت، برای رشته پس‌زمینه — 16–24 کیلوبایت. اندازه دقیق به نسخه Android و سازنده دستگاه بستگی دارد. ART از گسترش پویای پشته استفاده می‌کند، اما نه بیش از 2× مقدار اولیه.

آیا بازگشت دم می‌تواند از StackOverflowError جلوگیری کند؟

در Kotlin — بله، اگر متد با tailrec مشخص شده باشد. کامپایلر بازگشت دم را به تکرار تبدیل می‌کند و رشد پشته را کاملاً حذف می‌کند. در جاوا، بازگشت دم توسط JVM بهینه‌سازی نمی‌شود (بر خلاف زبان‌های تابعی مانند Scala).

چرا StackOverflowError در شبیه‌ساز رخ می‌دهد اما در دستگاه واقعی نه؟

اندازه پشته در شبیه‌ساز و دستگاه واقعی ممکن است متفاوت باشد. شبیه‌ساز از Desktop JVM با پشته معمولی 512–1024 کیلوبایت استفاده می‌کند، در حالی که Android ART — 32–48 کیلوبایت. خطا در ART زودتر از Desktop JVM ظاهر می‌شود.

StackOverflowError چه تفاوتی با OutOfMemoryError دارد؟

ناحیه حافظه: StackOverflowError — خطای پشته (فریم‌های فراخوانی)، OutOfMemoryError — خطای heap (اشیاء). StackOverflowError تقریباً همیشه توسط بازگشت ایجاد می‌شود، در حالی که OutOfMemoryError — توسط نشت حافظه یا اشیاء بزرگ.

خلاصه

  • StackOverflowError — سرریز پشته فراخوانی هنگام تجاوز از حد عمق بازگشت
  • عمق پشته در Android 512–1024 فریم در رشته اصلی است
  • بازگشت بی‌نهایت — علت اصلی؛ شرط پایه را در هر متد بازگشتی بررسی کنید
  • وابستگی‌های چرخه‌ای در سازنده‌ها — علت کمتر آشکار اما رایج سرریز
  • جایگزینی تکراری بازگشت از طریق Stack<T> صریح خطر را کاملاً از بین می‌برد
  • tailrec در Kotlin بازگشت دم را در سطح کامپایلر به تکرار تبدیل می‌کند
  • تحلیل استاتیک (Detekt, Infer) بازگشت‌های بی‌نهایت بالقوه را قبل از اجرا پیدا می‌کند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید