Stack Overflow في تطوير التطبيقات المحمولة — ما هو، أسبابه وطرق الوقاية

المؤلف: IT Sectr نُشر: 2026-03-29 وقت القراءة: 9 دق

Stack Overflow هو خطأ تجاوز سعة مكدس الاستدعاءات (java.lang.StackOverflowError) الذي يحدث عند تجاوز الحد الأقصى لعمق مكدس الخيط. وفقًا لمواصفات آلة جافا الافتراضية، يبلغ العمق النموذجي للمكدس في JVM 1024 إطارًا للأنظمة 64 بت. السبب الرئيسي هو العودية اللانهائية بدون شرط أساسي.

أهم النقاط

  • StackOverflowError — خطأ JVM عند تجاوز حد عمق مكدس الاستدعاءات
  • عمق المكدس محدود بـ 512–2048 إطارًا حسب التكوين
  • العودية اللانهائية هي السبب الأكثر شيوعًا لـ StackOverflowError
  • العودية الذيلية لا يتم تحسينها في JVM، على عكس اللغات الوظيفية
  • الاستبدال التكراري للعودية هو طريقة موثوقة لمنع الفائض

ما هو Stack Overflow

StackOverflowError هو خطأ قاتل في آلة جافا الافتراضية (JVM) أو Android Runtime (ART) يحدث عندما يصل مكدس الاستدعاءات لخيط ما إلى أقصى عمق مسموح به. على عكس OutOfMemoryError (نفاد Heap)، يرتبط StackOverflowError بمنطقة ذاكرة مختلفة — المكدس، حيث يتم تخزين إطارات استدعاءات الطرق والمتغيرات المحلية.

كل استدعاء لطريقة ينشئ إطارًا في المكدس: عنوان العودة، المعاملات والمتغيرات المحلية. عند العودة من الطريقة، يتم تدمير الإطار. إذا استدعت طريقة نفسها (عودية) بدون شرط أساسي، تتراكم الإطارات حتى يمتلئ المكدس. لا تستطيع JVM تخصيص إطار جديد وترمي StackOverflowError برسالة «null» (في Java) أو مع إشارة إلى سطر مكدس يتكرر بلا نهاية.

حجم مكدس الخيط ثابت عند الإنشاء ولا يتغير أثناء التنفيذ. في Android، الحجم النموذجي لمكدس الخيط الرئيسي هو 32–48 كيلوبايت، مما يعطي عمقًا يبلغ حوالي 512–1024 إطارًا للطرق التي لا تحتوي على العديد من المتغيرات المحلية. للخيوط الخلفية، الحجم الافتراضي أصغر — 16–24 كيلوبايت.

كيف يعمل مكدس الاستدعاءات

مكدس الاستدعاءات (Call Stack) هو بنية بيانات LIFO (Last In, First Out) تدير ترتيب تنفيذ الطرق. في كل مرة يستدعي البرنامج طريقة، تنشئ JVM إطارًا في المكدس وتضعه في الأعلى. عند اكتمال الطريقة، يتم إخراج الإطار.

كل إطار يحتوي على: مكدس المعاملات (لتعليمات البايت كود)، مصفوفة المتغيرات المحلية (بما في ذلك this)، مرجع إلى مجمع الثوابت وعنوان العودة. كلما زادت المتغيرات المحلية للطريقة، زاد حجم إطارها وقل عدد الطرق التي يمكن استدعاؤها قبل امتلاء المكدس. طريقة تحتوي على 10 معاملات و20 متغيرًا محليًا تشغل مساحة أكبر بحوالي 3 مرات من طريقة بدون معاملات.

في Android، يستخدم ART تطبيقه الخاص للمكدس، المختلف عن JVM المكتبية. يمكن لـ ART زيادة المكدس ديناميكيًا ضمن حدود معينة، لكن لا يزال هناك حد صارم لكل خيط. الخيط الرئيسي (خيط UI) لديه أكبر مكدس، حيث يتعامل مع دورة حياة النشاط بالكامل ومعالجة الأحداث.

kotlin
// عودية تؤدي إلى 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) تكتشف هذه الدورات في وقت الترجمة، لكن الإنشاء اليدوي للكائنات لا يكتشفها.

استخدم حقن التبعية مع رسوم بيانية للتبعيات: 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 عنصر. ViewGroup Android مع 20 مستوى من التداخل نادر، لكن التحليل العودي لـ JSON مع 2000 كائن متداخل هو سيناريو حقيقي.

استبدل الاجتياز العودي باجتياز تكراري عبر Stack<T> صريح أو ArrayDeque. هذا يزيل تمامًا خطر تجاوز سعة المكدس، لأن كائنات heap غير محدودة بحد المكدس. BFS (بحث العرض الأول) عبر Queue يحل المشكلة أيضًا.

معالجة غير صحيحة لـ onConfigurationChanged

سبب خاص بـ Android: استدعاءات دائرية لطرق دورة الحياة عند معالجة التكوين بشكل غير صحيح. على سبيل المثال، استدعاء recreate() داخل onConfigurationChanged، الذي يستدعي again onConfigurationChanged، وهكذا حتى StackOverflowError. بالمثل: setContentView() داخل onLayout()، الذي يسبب قياسًا وتخطيطًا آخر.

لا تستدعي recreate() داخل الطرق المتعلقة بتغييرات التكوين. لتحديث واجهة المستخدم عند تغيير السمة، استخدم setTheme() بدون recreate. للتغييرات الديناميكية في الاتجاه — استدعِ requestOrientation() مرة واحدة، بدون علامة في التكوين.

التسلسل مع المراجع الدائرية

Gson أو Moshi أو Kotlin Serialization عند محاولة تسلسل كائن بمراجع دائرية (A يشير إلى B، B يشير إلى A) يدخلون في عودية لا نهائية ويفشلون مع StackOverflowError. هذه مشكلة شائعة عند تسلسل الكيانات بعلاقات ثنائية الاتجاه (JPA, Room مع ForeignKey).

استخدم @Transient أو @JsonIgnore أو @kotlinx.serialization.Transient لجانب واحد من الدورة. لـ Gson — JsonSerializer مع حد عمق صريح. لـ Room — لا تسلسل Entity أبدًا مباشرة، استخدم مهام DTO.

كيفية تشخيص وإصلاح StackOverflowError

تشخيص StackOverflowError أسهل من أخطاء الذاكرة الأخرى: يظهر تتبع المكدس في معظم الحالات تسلسلًا متكررًا من الاستدعاءات. هذا يشير فورًا إلى العودية.

قراءة تتبع المكدس

تتبع المكدس لـ StackOverflowError فريد: بعد أول 200–500 سطر، يبدأ نفس نمط الاستدعاءات في التكرار. تقوم JVM باقتطاع الأسطر المتكررة في النهاية وتظهر «... 1234 more». عدد الأسطر غير المتكررة قبل «...» يشير إلى عمق العودية الذي تسبب في الخطأ.

اقرأ الأسطر الأولى من تتبع المكدس — تظهر من أي طريقة بدأ التكرار. ابحث عن الطريقة التي تستدعي نفسها أو تنشئ سلسلة استدعاءات تعود إليها. أصلح الشرط الأساسي أو استبدل العودية بحلقة.

زيادة حجم المكدس (حل مؤقت)

مؤقتًا، يمكن حل المشكلة بزيادة حجم المكدس عبر علامة 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 مستوى — لكن الخطأ سيظل يحدث، فقط لاحقًا. الحل الصحيح الوحيد هو الاستبدال التكراري للعودية.

استبدال العودية بالتكرار

الخوارزميات التكرارية لا تستخدم مكدس الاستدعاءات لتخزين الحالات الوسيطة — تخزنها في heap (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 هي مجموعة من القواعد والأدوات التي تحدد الدورات العودية المحتملة قبل وصولها إلى الإنتاج.

حد عمق العودية في بناء التصحيح

أضف عداد عمق وقائي في الطرق العودية في بناءات التصحيح. إذا تجاوز العمق حدًا معينًا (مثلاً 1000)، ارمِ استثناءً برسالة واضحة. هذا يحول StackOverflowError بتتبع غير قابل للقراءة إلى استثناء عمل مفهوم.

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 التي تحذر عن الاستدعاء الذاتي بدون تغيير المعاملات. فعّلها في مجموعة قواعد CI واضبط الخطورة على error.

مراجعة الكود مع التركيز على العودية

في مراجعة الكود، انتبه إلى: أي طرق ذات استدعاء ذاتي، استدعاءات عودية داخل لامبدا (دوال Kotlin المضمنة)، استدعاءات دائرية بين فئات مختلفة، عودية في مفوضيات الخصائص. لكل طريقة عودية، تحقق: هل هناك شرط أساسي، هل يتغير المعامل في كل خطوة، هل يضمن تغيير المعامل الوصول إلى الشرط الأساسي.

تحويل العودية الذيلية (محدود)

Kotlin يدعم معدِّل tailrec: إذا كانت الطريقة العودية موسومة بـ tailrec وكان الاستدعاء ذيليًا (آخر عملية)، يحوله المترجم إلى تكرار. ومع ذلك، يعمل tailrec فقط للاستدعاء الذاتي (الطريقة تستدعي نفسها مباشرة)، لا يعمل للعودية المتبادلة، وغير مدعوم في إصدارات 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؟

نعم، ولكن فقط على مستوى Java. Error، مثل Exception، هو Throwable. ومع ذلك، بعد StackOverflowError يتضرر المكدس — الإطارات التي لم تسع لا يمكن أن تكتمل بشكل صحيح. محاولة إنشاء كائن جديد في كتلة catch قد تسبب StackOverflowError آخر.

ما هو حجم المكدس الافتراضي في Android؟

للخيط الرئيسي — 32–48 كيلوبايت، للخيوط الخلفية — 16–24 كيلوبايت. يعتمد الحجم الدقيق على إصدار Android والشركة المصنعة للجهاز. يستخدم ART توسيع المكدس الديناميكي ولكن لا يزيد عن ضعف القيمة الأولية.

هل يمكن للعودية الذيلية منع StackOverflowError؟

في Kotlin — نعم، إذا كانت الطريقة موسومة بـ tailrec. يحول المترجم العودية الذيلية إلى تكرار، مما يلغي تمامًا نمو المكدس. في Java، لا يتم تحسين العودية الذيلية بواسطة JVM (على عكس اللغات الوظيفية مثل Scala).

لماذا يحدث StackOverflowError على المحاكي ولكن ليس على الجهاز؟

حجم المكدس على المحاكي والجهاز الحقيقي قد يختلف. يستخدم المحاكي JVM مكتبية بمكدس نموذجي 512–1024 كيلوبايت، بينما يستخدم Android ART 32–48 كيلوبايت. سيظهر الخطأ على ART في وقت أبكر من JVM المكتبية.

كيف يختلف StackOverflowError عن OutOfMemoryError؟

منطقة الذاكرة: StackOverflowError هو خطأ مكدس (إطارات استدعاءات)، OutOfMemoryError هو خطأ heap (كائنات). StackOverflowError دائمًا تقريبًا ناتج عن العودية، بينما OutOfMemoryError ناتج عن تسربات الذاكرة أو الكائنات الكبيرة.

الخلاصة

  • StackOverflowError — تجاوز سعة مكدس الاستدعاءات عند تجاوز حد عمق العودية
  • عمق المكدس في Android هو 512–1024 إطارًا على الخيط الرئيسي
  • العودية اللانهائية هي السبب الرئيسي؛ تحقق من الشرط الأساسي في كل طريقة عودية
  • التبعيات الدائرية في المنشئات — سبب أقل وضوحًا لكنه شائع للفائض
  • الاستبدال التكراري للعودية عبر Stack<T> صريح يزيل الخطر تمامًا
  • tailrec في Kotlin يحول العودية الذيلية إلى تكرار على مستوى المترجم
  • التحليل الثابت (Detekt, Infer) يجد العوديات اللانهائية المحتملة قبل التشغيل

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا