Stack Overflow — خطای سرریز پشته فراخوانی (java.lang.StackOverflowError) که هنگام تجاوز از حداکثر عمق پشته رشته رخ میدهد. بر اساس Java Virtual Machine Specification، عمق معمول پشته در JVM برای سیستمهای 64 بیتی 1024 فریم است. علت اصلی — بازگشت بینهایت بدون شرط پایه توقف.
نکات کلیدی
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 و پردازش رویدادها روی آن اجرا میشود.
// بازگشتی که منجر به 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 جایگزین کنید.
// وابستگی چرخهای — 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() یک بار، بدون 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 سادهتر از سایر خطاهای حافظه است: 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 — اندازه مورد نظر بر حسب بایت.
// ایجاد رشته با پشته افزایشیافته
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
مهم: افزایش پشته مشکل را حل نمیکند، فقط آن را به تأخیر میاندازد. با بازگشت 10,000 سطحی، پشته 64 کیلوبایتی با پشته 128 کیلوبایتی جایگزین میشود که 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 کار میکند (متد مستقیماً خودش را فراخوانی میکند)، برای بازگشت متقابل کار نمیکند و در نسخههای Kotlin سازگار با Android قبل از 1.5 پشتیبانی نمیشود.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // فراخوانی دم
}
سوالات متداول
میتوان، اما فقط در سطح جاوا. Error مانند Exception، Throwable است. با این حال پس از StackOverflowError پشته آسیب دیده است — فریمهایی که جا نشدهاند نمیتوانند به درستی کامل شوند. تلاش برای ایجاد یک شیء جدید در بلوک catch ممکن است StackOverflowError دیگری ایجاد کند.
برای رشته اصلی — 32–48 کیلوبایت، برای رشته پسزمینه — 16–24 کیلوبایت. اندازه دقیق به نسخه Android و سازنده دستگاه بستگی دارد. ART از گسترش پویای پشته استفاده میکند، اما نه بیش از 2× مقدار اولیه.
در Kotlin — بله، اگر متد با tailrec مشخص شده باشد. کامپایلر بازگشت دم را به تکرار تبدیل میکند و رشد پشته را کاملاً حذف میکند. در جاوا، بازگشت دم توسط JVM بهینهسازی نمیشود (بر خلاف زبانهای تابعی مانند Scala).
اندازه پشته در شبیهساز و دستگاه واقعی ممکن است متفاوت باشد. شبیهساز از Desktop JVM با پشته معمولی 512–1024 کیلوبایت استفاده میکند، در حالی که Android ART — 32–48 کیلوبایت. خطا در ART زودتر از Desktop JVM ظاهر میشود.
ناحیه حافظه: StackOverflowError — خطای پشته (فریمهای فراخوانی)، OutOfMemoryError — خطای heap (اشیاء). StackOverflowError تقریباً همیشه توسط بازگشت ایجاد میشود، در حالی که OutOfMemoryError — توسط نشت حافظه یا اشیاء بزرگ.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید