Що таке StackOverflowError?


439

Що таке a StackOverflowError, що викликає це, і як я повинен з ними боротися?


Розмір стека в java невеликий. І іноді, як-от багато рекурсивних дзвінків, ви стикаєтеся з цією проблемою. Ви можете переглянути дизайн коду за допомогою циклу. Ви можете знайти загальну схему дизайну для цього в цій URL- адресі
JNDanial

Один неочевидний спосіб отримати це: додати рядок new Object() {{getClass().newInstance();}};до якогось статичного контексту (наприклад, mainметоду). Не працює з контексту екземпляра (лише кидки InstantiationException).
Джон Макклайн

Відповіді:


408

Параметри та локальні змінні виділяються на стеку (при використанні посилальних типів об'єкт живе на купі, а змінна в посиланнях стека - об'єкт на купі). Стек зазвичай живе у верхньому кінці вашого адресного простору, і при використанні він спрямовується до нижньої частини адресного простору (тобто до нуля).

У вашому процесі також є купа , яка живе в нижній частині вашого процесу. Виділяючи пам'ять, ця купа може рости у верхньому кінці вашого адресного простору. Як бачимо, існує купа "зіткнення". зі стеком (трохи схоже на тектонічні плити !!!).

Частою причиною переповнення стека є поганий рекурсивний виклик . Як правило, це виникає, коли у ваших рекурсивних функцій немає правильної умови припинення, тому він закінчується викликати себе назавжди. Або коли умова припинення нормальна, це може бути викликано необхідністю занадто багато рекурсивних дзвінків перед її виконанням.

Однак за допомогою програмування GUI можливо генерувати непрямі рекурсії . Наприклад, ваша програма може обробляти повідомлення з фарбою, і, обробляючи їх, вона може викликати функцію, яка змушує систему надсилати інше повідомлення з фарбою. Тут ви прямо не називали себе, але OS / VM зробив це за вас.

Щоб боротися з ними, вам потрібно буде вивчити свій код. Якщо у вас є функції, які викликають себе, то перевірте, чи є у вас термін припинення. Якщо у вас є, то перевірте, що при виклику функції ви принаймні змінили один з аргументів, інакше не буде видимих ​​змін для рекурсивно викликаної функції, і умова завершення є марною. Також пам’ятайте, що у вашому просторі стеку може не вистачати пам’яті, перш ніж досягти дійсного умови завершення, таким чином переконайтеся, що ваш метод може обробляти вхідні значення, що вимагають більше рекурсивних викликів.

Якщо у вас немає очевидних рекурсивних функцій, перевірте, чи викликаєте ви будь-які функції бібліотеки, які побічно спричинить виклик вашої функції (як, наприклад, неявний випадок вище).


1
Оригінальний плакат: ей це чудово. Отже, рекурсія завжди відповідає за переповнення стека? Або можуть відповідати за них і інші речі? На жаль, я використовую бібліотеку ... але не ту, яку я розумію.
Ziggy

4
Ха-ха-ха, так ось це: while (бали <100) {addMouseListeners (); moveball (); перевірка перебігу (); пауза (швидкість);} Нічого не відчуваю, що я кульгаю за те, що я не розумію, що я закінчуся б великою кількістю слухачів миші ... Спасибі хлопці!
Ziggy

4
Ні, переповнення стека також можуть надходити від змінних, які занадто великі для виділення в стеку, якщо ви шукаєте статтю Wikipedia на ньому за адресою en.wikipedia.org/wiki/Stack_overflow .
JB King

8
Слід зазначити, що "обробити" помилку переповнення стека майже неможливо. У більшості середовищ, щоб обробити помилку, потрібно запустити код на стеці, що складно, якщо немає більше місця для стеку.
Гарячі лизання

3
@JB King: Насправді не застосовується до Java, де в стеці зберігаються лише примітивні типи та посилання. Всі великі речі (масиви та об’єкти) знаходяться на купі.
jcsahnwaldt каже, що GoFundMonica

107

Щоб описати це, спочатку давайте зрозуміємо, як зберігаються локальні змінні та об'єкти.

Локальна змінна зберігається в стеці : введіть тут опис зображення

Якщо ви подивилися на зображення, ви повинні мати можливість зрозуміти, як все працює.

Коли виклик функції викликається програмою Java, кадр стека виділяється на стек викликів. Кадр стека містить параметри викликаного методу, його локальні параметри та зворотну адресу методу. Зворотна адреса позначає точку виконання, з якої виконання програми триває після повернення викликаного методу. Якщо для нового фрейма стека немає місця, то StackOverflowErrorце викидається віртуальною машиною Java (JVM).

Найпоширеніший випадок, який, можливо, може вичерпати стек програми Java - це рекурсія. У рекурсії метод викликає себе під час його виконання. Рекурсія розглядається як потужна методика програмування загального призначення, але її слід використовувати обережно, щоб уникнути StackOverflowError.

Приклад кидання a StackOverflowErrorпоказаний нижче:

StackOverflowErrorExample.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 (JVM), розмір стека потоку за замовчуванням може дорівнювати або 512 КБ, або 1 МБ . Ви можете збільшити розмір стека нитки, використовуючи -Xssпрапор. Цей прапор можна вказати або через конфігурацію проекту, або через командний рядок. Формат -Xssаргументу: -Xss<size>[g|G|m|M|k|K]


Здається, що у деяких версіях java існує помилка при використанні Windows, де аргумент -Xss діє лише на нових
потоках

65

Якщо у вас є така функція:

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

Тоді foo () буде продовжувати дзвонити собі, стає все глибшим і глибшим, і коли простір, який використовується для відстеження того, які функції ви перебуваєте, заповнюється, ви отримуєте помилку переповнення стека.


12
Неправильно. Ваша функція хвостово-рекурсивна. Більшість компільованих мов мають оптимізацію хвостових рекурсій. Це означає, що рекурсія зводиться до простого циклу, і ви ніколи не потрапите в переповнення стека з цим фрагментом коду в деяких системах.
Веселий

Подивіться, які нефункціональні мови підтримують хвостову рекурсію?
horseyguy

@banister та деякі реалізації javascript
Pacerier

@horseyguy Scala підтримує рекурсію хвоста.
Ajit K'sagar

Це відображає суть того, що може створити переповнення стека. Приємно.
Піксель

24

Переповнення стека означає саме це: стек переповнює. Зазвичай в програмі є один стек, який містить змінні локальної області та адреси, куди повертатися, коли виконання рутини закінчується. Цей стек має тенденцію до фіксованого діапазону пам'яті десь у пам'яті, тому обмежено, скільки він може містити значень.

Якщо стек порожній, ви не можете скокнути, якщо у вас з'явиться помилка переповнення стека.

Якщо стек повний, ви не можете натискати, якщо у вас з'явиться помилка переповнення стека.

Тож переповнення стека з’являється там, де ви виділяєте занадто багато в стек. Наприклад, у згаданій рекурсії.

Деякі реалізації оптимізують деякі форми рекурсій. Зокрема, рекурсія хвоста. Хвостові рекурсивні підпрограми - це форма підпрограм, де рекурсивний виклик постає як остаточне, що робить рутина. Такий звичайний дзвінок просто скорочується в стрибок.

Деякі реалізації заходять настільки далеко, що реалізують власні стеки для рекурсії, тому вони дозволяють рекурсії тривати доти, поки в системі не закінчиться пам'ять.

Найпростіше, що ви можете спробувати, - збільшити розмір стека, якщо можете. Якщо ви цього не можете зробити, другою найкращою справою буде подивитися, чи є щось, що явно викликає переповнення стека. Спробуйте, надрукувавши щось до і після дзвінка в рутину. Це допомагає вам з’ясувати невдачу рутини.


4
Чи є таке поняття, як стек під потоком ?
Pacerier

5
Підбірка стека можлива в зборі (вискакує більше, ніж ви натиснули), хоча в компільованих мовах це було б майже неможливо. Я не впевнений, що ви, можливо, зможете знайти реалізацію аллока () C, яка "підтримує" негативні розміри.
Score_Under

2
Переповнення стека означає саме це: стек переповнює. Зазвичай в програмі є один стек, який містить змінні локальної області -> Ні, у кожного потоку є власний стек, який містить кадри стека для кожного виклику методу, який містить локальні змінні ..
Koray Tugay

9

Переповнення стека зазвичай викликається надто глибокими викликами функцій вкладених функцій (особливо це легко при використанні рекурсії, тобто функції, яка сама викликає) або виділенням великої кількості пам'яті на стеку, де більш зручним буде використання купі.


1
На жаль, не побачив тег Java
Грег,

Крім того, з оригінального плаката тут: функції гніздування занадто глибоко в чому? Інші функції? І: як можна виділити пам'ять на стек чи купу (оскільки, знаєте, я чітко зробив одну з цих речей, не знаючи).
Ziggy

@ Ziggy: Так, якщо одна функція викликає іншу функцію, яка викликає ще одну функцію тощо, після багатьох рівнів у вашої програми буде переповнено стек. [продовжує]
Кріс Єстер-Янг

[... продовження] У Java ви не можете безпосередньо виділити пам'ять зі стека (тоді як у C ви можете, і це тоді буде на що дивитися), тому це навряд чи буде причиною. У Java всі прямі виділення надходять з купи, використовуючи "new".
Кріс Єстер-Янг

@ ChrisJester-Young Чи не правда, що якщо у мене є 100 локальних змінних у методі, все це виходить на стек без винятків?
Пейсьєр

7

Як ви кажете, вам потрібно показати якийсь код. :-)

Помилка переповнення стека зазвичай трапляється, коли ваша функція викликає гніздо занадто глибоко. Перегляньте нитку Golf Stack Code Overflow для деяких прикладів того, як це відбувається (хоча у випадку з цим питанням відповіді навмисно викликають переповнення стека).


1
Я б повністю хотів додати код, але оскільки я не знаю, що викликає переповнення стека, я не знаю, який код додати. додавання всього коду було б кульгавим, ні?
Ziggy

Ваш проект з відкритим кодом? Якщо так, просто зробіть рахунок Sourceforge або github і завантажте туди свій код. :-)
Кріс Єстер-Янг

це звучить як чудова ідея, але я такий ноб, що навіть не знаю, що мені доведеться завантажувати. Мовляв, бібліотека, в якій я імпортую класи, які я розширюю і т. Д. ... для мене все невідомі. О людино: погані часи.
Ziggy


5

StackOverflowErrorдо стека, як OutOfMemoryErrorдо купи.

Безмежні рекурсивні дзвінки призводять до використання простору стеку.

Наступний приклад дає StackOverflowError:

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

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

StackOverflowError можна уникнути, якщо рекурсивні дзвінки обмежуються, щоб запобігти перевищенню сукупної кількості неповних викликів в пам'яті (у байтах) розмір стека (у байтах).


3

Ось приклад рекурсивного алгоритму зворотного зв'язку окремо пов'язаного списку. На ноутбуці з такою специфікацією (4G пам'ять, процесор Intel Core i5 2.3 ГГц процесор, 64-розрядна Windows 7) ця функція зіткнеться з помилкою StackOverflow для пов'язаного списку розміром близько 10 000.

Моя думка, що ми повинні використовувати рекурсію розумно, завжди з урахуванням масштабу системи. Часто рекурсію можна перетворити на ітераційну програму, яка краще масштабується. (Одна ітеративна версія того ж алгоритму наведена внизу сторінки, вона реверсує окремо пов'язаний список розміром 1 мільйон за 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);
}

Я думаю, що для JVM це насправді не має значення, якою є специфікація вашого ноутбука.
кевін

3

A StackOverflowError- помилка виконання у Java.

Він викидається при перевищенні обсягу пам'яті стека викликів, виділеного 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)

У наведеному вище випадку цього можна уникнути, змінивши програмні зміни. Але якщо логіка програми правильна і вона все-таки виникає, то розмір стека потрібно збільшити.


0

Це типовий випадок 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


0

У випадку крихти, нижче ситуація призведе до помилки переповнення стека.

public class Example3 {

public static void main(String[] args) {

    main(new String[1]);

}

}


-1

Ось приклад

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

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

В основному, StackOverflowError - це коли ви намагаєтесь щось зробити, що, швидше за все, викликає себе і продовжує нескінченність (або поки він не дає StackOverflowError).

add5(a) зателефонує собі, а потім знову зателефонує собі тощо.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.