سؤال

ما هو StackOverflowError, وما أسبابها وكيف أتعامل معها؟

هل كانت مفيدة؟

المحلول

يتم تخصيص المعلمات والمتغيرات المحلية على كومة (مع الأنواع المرجعية ، يعيش الكائن على كومة ومتغير في المراجع المكدس التي الكائن على الكومة). عادة ما يعيش المكدس في العلوي نهاية مساحة العنوان الخاصة بك ، كما يتم استخدامه يتجه نحو الأسفل من مساحة العنوان (أي نحو الصفر).

عمليتك لديها أيضا ملف كومة, الذي يعيش في الأسفل نهاية عمليتك. أثناء تخصيص الذاكرة ، يمكن أن تنمو هذه الكومة نحو الطرف العلوي من مساحة عنوانك. كما ترون ، هناك إمكانية لكومة "تصادم" مع المكدس (مثل لوحات التكتونية !!!).

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

ومع ذلك ، مع برمجة واجهة المستخدم الرسومية ، من الممكن توليدها عودة غير مباشرة. على سبيل المثال ، قد يكون التطبيق الخاص بك هو التعامل مع رسائل الطلاء ، وبينما يعالجها ، قد يتصل بوظيفة تسبب النظام لإرسال رسالة طلاء أخرى. هنا لم تسمي نفسك صراحة ، لكن نظام التشغيل/VM قام بذلك من أجلك.

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

إذا لم يكن لديك وظائف متكررة واضحة ، فتحقق لمعرفة ما إذا كنت تتصل بأي وظائف مكتبة بشكل غير مباشر سوف يتسبب في استدعاء وظيفتك (مثل الحالة الضمنية أعلاه).

نصائح أخرى

لوصف هذا ، دعونا أولاً نفهم كيف محلي يتم تخزين المتغيرات والكائنات.

يتم تخزين المتغير المحلي في كومة: enter image description here

إذا نظرت إلى الصورة ، فيجب أن تكون قادرًا على فهم كيفية عمل الأشياء.

عندما يتم استدعاء استدعاء الوظيفة بواسطة تطبيق Java ، يتم تخصيص إطار المكدس على مكدس المكالمات. يحتوي إطار المكدس على معلمات الطريقة التي تم استدعاؤها ومعلماتها المحلية وعنوان إرجاع الطريقة. يشير عنوان الإرجاع إلى نقطة التنفيذ التي يجب أن يستمر تنفيذ البرنامج بعد عودة الطريقة التي تم استدعاؤها. إذا لم يكن هناك مساحة لإطار مكدس جديد ، فإن StackOverflowError يتم إلقاؤه بواسطة الجهاز الظاهري Java (JVM).

الحالة الأكثر شيوعًا التي يمكن أن تستنفد كومة تطبيق Java هي العودية. في العودية ، تستدعي الطريقة نفسها أثناء تنفيذها. يعتبر العودية تقنية برمجة قوية للأغراض العامة ، ولكن يجب استخدامها بحذر ، لتجنب StackOverflowError.

مثال على رمي أ StackOverflowError يظهر أدناه:

StackOverFlowerRorerexample.java:

public class StackOverflowErrorExample {

    public static void recursivePrint(int num) {
        System.out.println("Number: " + num);

        if(num == 0)
            return;
        else
            recursivePrint(++num);
    }

    public static void main(String[] args) {
        StackOverflowErrorExample.recursivePrint(1);
    }
}

في هذا المثال ، نحدد طريقة متكررة ، تسمى recursivePrint يطبع عدد صحيح ثم يدعو نفسه ، مع عدد صحيح متتالي التالي كحجة. ينتهي العودية حتى نمر 0 كمعلمة. ومع ذلك ، في مثالنا ، مررنا في المعلمة من 1 وأتباعها المتزايدين ، وبالتالي لن ينتهي العودية أبدًا.

تنفيذ عينة ، باستخدام -Xss1M العلم الذي يحدد حجم مكدس الخيط الذي يساوي 1 ميجابايت ، يظهر أدناه:

Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
        at java.io.PrintStream.write(PrintStream.java:480)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.write(PrintStream.java:527)
        at java.io.PrintStream.print(PrintStream.java:669)
        at java.io.PrintStream.println(PrintStream.java:806)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
        ...

اعتمادًا على التكوين الأولي لـ JVM ، قد تختلف النتائج ، ولكن في النهاية StackOverflowError يجب إلقاؤه. هذا المثال هو مثال جيد للغاية على كيفية تسبب العودية في حدوث مشاكل ، إن لم يتم تنفيذها بحذر.

كيفية التعامل مع stackoverflowerror

  1. أبسط الحلول هو فحص تتبع المكدس بعناية والكشف عن نمط التكرار لأرقام الخط. تشير أرقام الأسطر هذه إلى الرمز الذي يتم استدعاؤه بشكل متكرر. بمجرد اكتشاف هذه الخطوط ، يجب عليك فحص الكود الخاص بك بعناية وفهم لماذا لا ينتهي العودية أبدًا.

  2. إذا كنت قد أوضحت أن العودية يتم تنفيذها بشكل صحيح ، فيمكنك زيادة حجم المكدس ، من أجل السماح لعدد أكبر من الدعوات. اعتمادًا على جهاز Java Virtual Machine (JVM) المثبت ، قد يساوي حجم مكدس مؤشر الترابط الافتراضي أي منهما 512 كيلو بايت ، أو 1 ميجابايت. يمكنك زيادة حجم مكدس الخيط باستخدام -Xss علَم. يمكن تحديد هذه العلامة إما عبر تكوين المشروع ، أو عبر سطر الأوامر. شكل -Xss الحجة هي: -Xss<size>[g|G|m|M|k|K]

إذا كان لديك وظيفة مثل:

int foo()
{
    // more stuff
    foo();
}

ثم ستواصل FOO () الاتصال بنفسها ، والتعمق وأعمق ، وعندما تتم ملء المساحة المستخدمة لتتبع الوظائف التي تتواجد فيها ، ستحصل على خطأ فائض في التدفق.

تجاوز سعة المكدس يعني بالضبط ما يلي:مكدس يفيض.عادةً ما يكون هناك مكدس واحد في البرنامج يحتوي على متغيرات النطاق المحلي والعناوين التي يمكن إرجاعها عند انتهاء تنفيذ الروتين.يميل هذا المكدس إلى أن يكون نطاقًا ثابتًا للذاكرة في مكان ما في الذاكرة، وبالتالي فإن مقدار القيم التي يمكن أن يحتوي عليها محدود.

إذا كانت المكدسة فارغة، فلا يمكنك فرقعها، وإذا قمت بذلك فسوف تحصل على خطأ في تجاوز سعة المكدس.

إذا كانت المكدس ممتلئة فلا يمكنك الدفع، وإذا قمت بذلك فسوف تحصل على خطأ تجاوز سعة المكدس.

لذلك يظهر تجاوز سعة المكدس حيث تقوم بتخصيص الكثير في المكدس.على سبيل المثال، في العودية المذكورة.

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

تذهب بعض التطبيقات إلى حد تنفيذ مكدساتها الخاصة للتكرار، وبالتالي فهي تسمح باستمرار التكرار حتى نفاد ذاكرة النظام.

أسهل شيء يمكنك تجربته هو زيادة حجم المكدس الخاص بك إذا استطعت.إذا لم تتمكن من القيام بذلك، فإن ثاني أفضل شيء هو التحقق مما إذا كان هناك شيء يتسبب بوضوح في تجاوز سعة المكدس.جرب ذلك عن طريق طباعة شيء ما قبل وبعد المكالمة في الروتين.هذا يساعدك على معرفة الروتين الفاشل.

عادةً ما يتم استدعاء تدفق المكدس من خلال استدعاء وظيفة التعشيش بعمق للغاية (بشكل خاص عند استخدام العودية ، أي وظيفة تدعو نفسها) أو تخصيص كمية كبيرة من الذاكرة على المكدس حيث يكون استخدام الكومة أكثر ملاءمة.

كما تقول ، تحتاج إلى إظهار بعض التعليمات البرمجية. :-)

يحدث خطأ في التدفق المكدس عادة عندما تستدعي وظيفتك العشق العميقة. انظر مكدس فائض رمز الجولف موضوع لبعض الأمثلة على كيفية حدوث ذلك (على الرغم من أنه في حالة هذا السؤال ، تتسبب الإجابات عمداً في تدفق المكدس).

السبب الأكثر شيوعًا للفيضان المكدس هو عودة عميقة أو لا حصر لها. إذا كانت هذه هي مشكلتك ، هذا البرنامج التعليمي حول عودة جافا يمكن أن تساعد في فهم المشكلة.

StackOverflowError هو إلى المكدس OutOfMemoryError هو للكومة.

تؤدي المكالمات العودية غير المحدودة إلى استخدام مساحة المكدس.

المثال التالي ينتج StackOverflowError:

class  StackOverflowDemo
{
    public static void unboundedRecursiveCall() {
     unboundedRecursiveCall();
    }

    public static void main(String[] args) 
    {
        unboundedRecursiveCall();
    }
}

StackOverflowError يمكن تجنبها إذا كانت المكالمات العودية محدودة لمنع إجمالي الإجمالي للمكالمات غير المكتملة في الذاكرة (بالبايت) من تجاوز حجم المكدس (بالبايت).

فيما يلي مثال على خوارزمية عودية لعكس قائمة مرتبطة منفردة.على جهاز كمبيوتر محمول بالمواصفات التالية (ذاكرة 4G، وحدة المعالجة المركزية Intel Core i5 2.3 جيجا هرتز، 64 بت Windows 7)، ستواجه هذه الوظيفة خطأ StackOverflow لقائمة مرتبطة بحجم قريب من 10000.

وجهة نظري هي أننا يجب أن نستخدم العودية بحكمة، مع الأخذ في الاعتبار دائمًا حجم النظام.في كثير من الأحيان يمكن تحويل العودية إلى برنامج تكراري، والذي يتوسع بشكل أفضل.(توجد نسخة متكررة لنفس الخوارزمية في أسفل الصفحة، وهي تعكس قائمة مرتبطة منفردة بحجم مليون في 9 مللي ثانية.)

    private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){

    LinkedListNode second = first.next;

    first.next = x;

    if(second != null){
        return doReverseRecursively(first, second);
    }else{
        return first;
    }
}

public static LinkedListNode reverseRecursively(LinkedListNode head){
    return doReverseRecursively(null, head);
}

النسخة التكرارية لنفس الخوارزمية:

    public static LinkedListNode reverseIteratively(LinkedListNode head){
    return doReverseIteratively(null, head);
}   

private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {

    while (first != null) {
        LinkedListNode second = first.next;
        first.next = x;
        x = first;

        if (second == null) {
            break;
        } else {
            first = second;
        }
    }
    return first;
}


public static LinkedListNode reverseIteratively(LinkedListNode head){
    return doReverseIteratively(null, head);
}

أ StackOverflowError هو خطأ في وقت التشغيل في جافا.

يتم إلقاؤه عندما يتم تجاوز كمية ذاكرة مكدس المكالمة التي يتم تخصيصها بواسطة JVM.

حالة شائعة من StackOverflowError يتم إلقاؤه ، عندما يتجاوز مكدس المكالمة بسبب التكرار العميق أو اللانهائي.

مثال:

public class Factorial {
    public static int factorial(int n){
        if(n == 1){
            return 1;
        }
        else{
            return n * factorial(n-1);
        }
    }

    public static void main(String[] args){
        System.out.println("Main method started");
        int result = Factorial.factorial(-1);
        System.out.println("Factorial ==>"+result);
        System.out.println("Main method ended");
    }
}

تتبع المكدس:

Main method started
Exception in thread "main" java.lang.StackOverflowError
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)

في الحالة أعلاه ، يمكن تجنبها من خلال إجراء تغييرات برمجية. ولكن إذا كان منطق البرنامج صحيحًا ولا يزال يحدث ، فيجب زيادة حجم المكدس.

هذا مثال

public static void main(String[] args) {
    System.out.println(add5(1));
}

public static int add5(int a) {
    return add5(a) + 5;
}

يتمثل stackoverflowerror في الأساس عندما تحاول القيام بشيء ما ، على الأرجح يدعو نفسه ، ويستمر في Infinity (أو حتى يعطي stackoverflowerror).

add5(a) سوف نسمي نفسها ، ثم اتصل بنفسها مرة أخرى ، وهكذا.

هذه حالة نموذجية java.lang.StackOverflowError... الطريقة تدعو نفسها بشكل متكرر دون خروج في doubleValue(), floatValue(), ، إلخ.

Rational.java

    public class Rational extends Number implements Comparable<Rational> {
        private int num;
        private int denom;

        public Rational(int num, int denom) {
            this.num = num;
            this.denom = denom;
        }

        public int compareTo(Rational r) {
            if ((num / denom) - (r.num / r.denom) > 0) {
                return +1;
            } else if ((num / denom) - (r.num / r.denom) < 0) {
                return -1;
            }
            return 0;
        }

        public Rational add(Rational r) {
            return new Rational(num + r.num, denom + r.denom);
        }

        public Rational sub(Rational r) {
            return new Rational(num - r.num, denom - r.denom);
        }

        public Rational mul(Rational r) {
            return new Rational(num * r.num, denom * r.denom);
        }

        public Rational div(Rational r) {
            return new Rational(num * r.denom, denom * r.num);
        }

        public int gcd(Rational r) {
            int i = 1;
            while (i != 0) {
                i = denom % r.denom;
                denom = r.denom;
                r.denom = i;
            }
            return denom;
        }

        public String toString() {
            String a = num + "/" + denom;
            return a;
        }

        public double doubleValue() {
            return (double) doubleValue();
        }

        public float floatValue() {
            return (float) floatValue();
        }

        public int intValue() {
            return (int) intValue();
        }

        public long longValue() {
            return (long) longValue();
        }
    }

main.java

    public class Main {

        public static void main(String[] args) {

            Rational a = new Rational(2, 4);
            Rational b = new Rational(2, 6);

            System.out.println(a + " + " + b + " = " + a.add(b));
            System.out.println(a + " - " + b + " = " + a.sub(b));
            System.out.println(a + " * " + b + " = " + a.mul(b));
            System.out.println(a + " / " + b + " = " + a.div(b));

            Rational[] arr = {new Rational(7, 1), new Rational(6, 1),
                    new Rational(5, 1), new Rational(4, 1),
                    new Rational(3, 1), new Rational(2, 1),
                    new Rational(1, 1), new Rational(1, 2),
                    new Rational(1, 3), new Rational(1, 4),
                    new Rational(1, 5), new Rational(1, 6),
                    new Rational(1, 7), new Rational(1, 8),
                    new Rational(1, 9), new Rational(0, 1)};

            selectSort(arr);

            for (int i = 0; i < arr.length - 1; ++i) {
                if (arr[i].compareTo(arr[i + 1]) > 0) {
                    System.exit(1);
                }
            }


            Number n = new Rational(3, 2);

            System.out.println(n.doubleValue());
            System.out.println(n.floatValue());
            System.out.println(n.intValue());
            System.out.println(n.longValue());
        }

        public static <T extends Comparable<? super T>> void selectSort(T[] array) {

            T temp;
            int mini;

            for (int i = 0; i < array.length - 1; ++i) {

                mini = i;

                for (int j = i + 1; j < array.length; ++j) {
                    if (array[j].compareTo(array[mini]) < 0) {
                        mini = j;
                    }
                }

                if (i != mini) {
                    temp = array[i];
                    array[i] = array[mini];
                    array[mini] = temp;
                }
            }
        }
    }

نتيجة

    2/4 + 2/6 = 4/10
    Exception in thread "main" java.lang.StackOverflowError
    2/4 - 2/6 = 0/-2
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
    2/4 * 2/6 = 4/24
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
    2/4 / 2/6 = 12/8
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
        at com.xetrasu.Rational.doubleValue(Rational.java:64)
        at com.xetrasu.Rational.doubleValue(Rational.java:64)

هنا هو رمز المصدر لـ StackOverflowError في OpenJdk 7

غالبًا ما يتم استخدام مصطلح "Overrun (Overflow)" ولكن تسمية خاطئة ؛ الهجمات لا تتفوق على المكدس ولكن المخازن المؤقتة على المكدس.

- من شرائح المحاضرات البروفيسور الدكتور ديتر جولمان

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top