Как обрабатывать StackOverflowError в Java? - PullRequest
16 голосов
/ 04 июня 2009

Как обработать StackOverflowError в Java?

Ответы [ 13 ]

17 голосов
/ 04 июня 2009

Возможно, у вас есть какая-то бесконечная рекурсия.

т.е. метод, который вызывает себя снова и снова

public void sillyMethod()
{
    sillyMethod();
}

Один из способов решить эту проблему - исправить ваш код так, чтобы рекурсия заканчивалась, а не продолжалась вечно.

16 голосов
/ 04 июня 2009

Я не уверен, что вы имеете в виду под "ручкой".

Вы, безусловно, можете поймать эту ошибку:

public class Example {
    public static void endless() {
        endless();
    }

    public static void main(String args[]) {
        try {
            endless();
        } catch(StackOverflowError t) {
            // more general: catch(Error t)
            // anything: catch(Throwable t)
            System.out.println("Caught "+t);
            t.printStackTrace();
        }
        System.out.println("After the error...");
    }
}

но это, скорее всего, плохая идея, если вы точно не знаете, что делаете.

13 голосов
/ 04 июня 2009

Взгляните на сообщение Раймонда Чена При отладке переполнения стека вы хотите сосредоточиться на повторяющейся рекурсивной части . Выписка:

Если вы отправляетесь на поиски в своей базе данных отслеживания дефектов, пытаясь определить, является ли это известной проблемой или нет, поиск верхних функций в стеке вряд ли найдет что-нибудь интересное. Это связано с тем, что переполнение стека обычно происходит в случайной точке рекурсии; каждое переполнение стека выглядит поверхностно отличным от любого другого, даже если это одно и то же переполнение стека.

Предположим, вы поете песню Frère Jacques , за исключением того, что вы поете каждый стих на несколько тонов выше, чем предыдущий. В конце концов, вы достигнете вершины своего пения, и то, где это произойдет, зависит от того, где ваш вокальный лимит совпадает с мелодией. В мелодии первые три ноты - это каждый новый «рекордный максимум» (т. Е. Ноты выше, чем любая другая, исполняемая до сих пор), а новые три максимума появляются в трех нотах третьего такта и финальной записи высоко во второй ноте пятой меры.

Если мелодия представляет использование стека программы, переполнение стека может произойти в любом из этих пяти мест выполнения программы. Другими словами, та же самая скрытая рекурсия (музыкально представленная во все более высоком исполнении мелодии) может проявляться пятью различными способами. «Рекурсия» в этой аналогии была довольно быстрой, всего за восемь тактов до повторения цикла. В реальной жизни цикл может быть довольно длинным, приводя к десяткам потенциальных точек, в которых может проявиться переполнение стека.

Если вы столкнулись с переполнением стека, то вы хотите игнорировать верхнюю часть стека, поскольку это просто фокус на конкретной ноте, которая превысила ваш голосовой диапазон. Вы действительно хотите найти всю мелодию, так как это то, что является общим для всех переполнений стека с той же основной причиной.

7 голосов
/ 04 июня 2009

Возможно, вы захотите узнать, поддерживается ли опция "-Xss" вашей JVM. Если это так, вы можете попытаться установить для него значение 512 КБ (по умолчанию 256 КБ в 32-разрядных версиях Windows и Unix) и посмотреть, что из этого выйдет (кроме как заставить вас сидеть дольше до вашего StackOverflowException). Обратите внимание, что это настройка для каждого потока, поэтому, если у вас запущено много потоков, вы также можете увеличить настройки кучи.

4 голосов
/ 05 июля 2012

Правильный ответ - тот, который уже дан. Вы, вероятно, либо: а) имеете ошибку в своем коде, приводящую к бесконечной рекурсии, которую обычно довольно легко диагностировать и исправлять, либо б) имеете код, который может привести к очень глубоким рекурсиям, например, рекурсивно обходить несбалансированное двоичное дерево. В последнем случае вам нужно изменить свой код, чтобы не размещать информацию в стеке (т.е. не повторять), а вместо этого размещать ее в куче.

Например, для несбалансированного обхода дерева вы можете хранить узлы, которые необходимо будет пересмотреть, в структуре данных стека. Для обхода по порядку вы должны были бы зацикливаться на левых ветвях, толкая каждый узел, когда вы посещали его, до тех пор, пока вы не дойдете до листа, который вы будете обрабатывать, затем вытолкнуть узел из верхней части стека, обработать его, а затем перезапустить цикл с правый дочерний элемент (просто установив переменную цикла в правый узел.) Это будет использовать постоянный объем стека, перемещая все, что было в стеке, в кучу в структуре данных стека. Куча, как правило, гораздо больше, чем стек.

Как то, что обычно является чрезвычайно плохой идеей, но необходимо в случаях, когда использование памяти чрезвычайно ограничено, вы можете использовать обращение указателя. В этом методе вы кодируете стек в структуру, которую вы пересекаете, и, повторно используя проходы, которые вы пересекаете, вы можете сделать это без или значительно меньше дополнительной памяти. Используя приведенный выше пример, вместо того, чтобы выдвигать узлы, когда мы выполняем цикл, нам просто нужно запомнить нашего непосредственного родителя, и на каждой итерации мы устанавливаем ссылку, по которой мы перешли, на текущего родителя, а затем текущего родителя на узел, который мы покидаем. Когда мы добираемся до листа, мы обрабатываем его, затем идем к нашему родителю, и тогда у нас есть загадка. Мы не знаем, исправить ли левую ветвь, обработать этот узел и продолжить с правой веткой, или исправить правую ветку и перейти к нашему родителю. Поэтому нам нужно выделить дополнительный бит информации во время итерации. Как правило, для низкоуровневых реализаций этого метода этот бит будет храниться в самом указателе, что приведет к отсутствию дополнительной памяти и общей постоянной памяти. Это не вариант в Java, но может быть возможно сжать этот бит в полях, используемых для других вещей. В худшем случае это по-прежнему как минимум 32-64-кратное сокращение объема необходимой памяти. Конечно, этот алгоритм чрезвычайно легко ошибиться с совершенно запутанными результатами и вызовет полный хаос с параллелизмом. Так что это почти никогда не стоит кошмар обслуживания, за исключением случаев, когда выделение памяти невозможно. Типичный пример - сборщик мусора, в котором распространены подобные алгоритмы.

Однако я действительно хотел поговорить о том, когда вы, возможно, захотите обработать ошибку StackOverflowError. А именно, чтобы обеспечить устранение хвостового вызова на JVM. Один из подходов заключается в использовании стиля батута, когда вместо выполнения хвостового вызова вы возвращаете объект с нулевой процедурой, или если вы просто возвращаете значение, вы его возвращаете. [Примечание: для этого требуются некоторые средства, чтобы сказать, что функция возвращает либо A, либо B. В Java, вероятно, самый легкий способ сделать это - вернуть один тип нормально и выбросить другой как исключение.] Затем, когда вы вызываете метод, вы нужно сделать цикл while, вызывающий нулевые процедуры (которые сами возвратят нулевую процедуру или значение), пока вы не получите значение. Бесконечный цикл станет циклом while, который постоянно заставляет объекты процедур возвращать объекты процедур. Преимущества стиля trampoline заключаются в том, что он использует только постоянный фактор большего стека, чем вы бы использовали с реализацией, которая должным образом исключает все хвостовые вызовы, он использует обычный стек Java для не-хвостовых вызовов, простой перевод, и он только увеличивает код с помощью (утомительного) постоянного фактора. Недостатком является то, что вы выделяете объект при каждом вызове метода (который сразу же становится мусором), и использование этих объектов включает в себя пару косвенных вызовов на один вызов хвоста.

В идеале лучше никогда не выделять эти нулевые процедуры или что-либо еще, что в точности и будет достигнуто устранением хвостового вызова. Работая с тем, что обеспечивает Java, мы могли бы выполнять код как обычно и выполнять эти нулевые процедуры только тогда, когда у нас закончился стек. Теперь мы по-прежнему распределяем эти бесполезные фреймы, но делаем это в стеке, а не в куче, и освобождаем их навалом, также наши вызовы являются обычными прямыми вызовами Java. Самый простой способ описать это преобразование - сначала переписать все методы с несколькими операторами вызова в методы, которые имеют два оператора вызова, т.е. fgh () {f (); г(); час(); } становится fgh () {f (); GH (); } и gh () {g (); час(); }. Для простоты я предполагаю, что все методы заканчиваются хвостовым вызовом, который можно упорядочить, просто упаковав оставшуюся часть метода в отдельный метод, хотя на практике вы захотите обработать их напрямую. После этих преобразований у нас есть три случая: либо метод имеет нулевые вызовы, в этом случае нечего делать, либо он имеет один (хвостовой) вызов, и в этом случае мы заключаем его в блок try-catch в то же самое, что и для хвостовой вызов в случае двух вызовов. Наконец, он может иметь два вызова: не-хвостовой и конечный, и в этом случае мы применяем следующее преобразование, проиллюстрированное примером (используя лямбда-нотацию C #, которую можно легко заменить на анонимный внутренний класс с некоторым ростом):

// top-level handler
Action tlh(Action act) {
    return () => {
        while(true) {
            try { act(); break; } catch(Bounce e) { tlh(() => e.run())(); }
        }
    }
}

gh() {
    try { g(); } catch(Bounce e) { 
        throw new Bounce(tlh(() => { 
            e.run(); 
            try { h(); } catch(StackOverflowError e) {
                throw new Bounce(tlh(() => h());
            }
        }); 
    }
    try { h(); } catch(StackOverflowError e) { 
        throw new Bounce(tlh(() => h())); 
    }
}

Основным преимуществом здесь является то, что если не выдается исключение, это тот же код, который мы начали с только с некоторыми дополнительными обработчиками исключений. Поскольку хвостовые вызовы (вызов h ()) не обрабатывают исключение Bounce, это исключение будет проходить через них, разматывая эти (ненужные) кадры из стека. Не-хвостовые вызовы перехватывают исключения Bounce и перебрасывают их с добавлением оставшегося кода. Это размотает стек до самого верхнего уровня, исключив кадры хвостовых вызовов, но запомнив кадры не хвостовых вызовов в нулевой процедуре. Когда мы, наконец, выполним процедуру в исключении Bounce на верхнем уровне, мы воссоздадим все фреймы вызова не-хвоста. На этом этапе, если мы сразу же снова исчерпаем стек, то, поскольку мы не переустанавливаем обработчики StackOverflowError, он будет отключен по желанию, поскольку мы действительно вне стека. Если мы пойдем немного дальше, новый StackOverflowError будет установлен соответствующим образом. Более того, если мы делаем успехи, но затем снова исчерпываем стек, нет смысла перематывать кадры, которые мы уже размотали, поэтому мы устанавливаем новые обработчики верхнего уровня, так что стек будет только разматываться до них.

Самая большая проблема с этим подходом состоит в том, что вы, вероятно, захотите вызывать обычные методы Java, и у вас может быть произвольно мало стекового пространства, когда у вас есть, поэтому у них может быть достаточно места для начала, но не до конца, и вы не можете возобновить их в середине. Есть как минимум два решения для этого. Первый - отправить всю такую ​​работу в отдельный поток, который будет иметь свой собственный стек. Это довольно эффективно и довольно просто и не вводит какой-либо параллелизм (если вы этого не хотите). Другой вариант - просто преднамеренно разматывать стек перед вызовом любого обычного метода Java, просто бросая StackOverflowError непосредственно перед ними. Если при возобновлении работы в нем по-прежнему не хватает места в стеке, значит, для начала вы были испорчены.

Подобную вещь можно сделать, чтобы сделать продолжения точно в срок. К сожалению, это преобразование на самом деле невозможно сделать вручную в Java, и, вероятно, оно граничит с такими языками, как C # или Scala. Таким образом, подобные преобразования обычно выполняются языками, нацеленными на JVM, а не людьми.

1 голос
/ 01 июня 2013

Большинство шансов получить StackOverflowError при использовании [длинных / бесконечных] рекурсий в рекурсивных функциях.

Вы можете избежать рекурсии функции, изменив дизайн своего приложения, чтобы использовать стекируемые объекты данных. Существуют шаблоны кодирования для преобразования рекурсивных кодов в блоки итеративного кода. Посмотрите на ответы ниже:

Таким образом, вы избегаете стекирования памяти Java для ваших рецессивных вызовов функций, используя ваши собственные стеки данных.

1 голос
/ 04 июня 2009

Полагаю, вы не можете - или, по крайней мере, это зависит от используемого вами jvm. Переполнение стека означает, что у вас нет места для хранения локальных переменных и возврата адресов. Если ваш jvm выполняет некоторую форму компиляции, у вас также есть стековый поток в jvm, и это означает, что вы не можете его обработать или перехватить. JVM должен прекратить.

Может быть способ создать jvm, который допускает такое поведение, но он будет медленным.

Я не проверял поведение с jvm, но в .net вы просто не можете обработать переполнение стека. Даже попытка поймать не поможет. Поскольку java и .net используют одну и ту же концепцию (виртуальные машины с jit), я подозреваю, что java будет вести себя одинаково. Присутствие исключения в стеке в потоке .NET предполагает, что может быть какой-то виртуальный компьютер, который позволяет программе перехватить его, а обычный - нет.

0 голосов
/ 24 июня 2014

в некоторых случаях вы не можете поймать stackoverflowerror. всякий раз, когда вы пытаетесь, вы сталкиваетесь с новым. потому что это Java vm. хорошо найти рекурсивные блоки кода, например, Эндрю Баллок сказал .

0 голосов
/ 04 декабря 2012
/*
Using Throwable we can trap any know error in JAVA..
*/
public class TestRecur {
    private int i = 0;


    public static void main(String[] args) {
        try {
            new TestRecur().show();
        } catch (Throwable err) {
            System.err.println("Error...");
        }
    }

    private void show() {
        System.out.println("I = " + i++);
        show();
    }
}

Однако вы можете взглянуть на ссылку: http://marxsoftware.blogspot.in/2009/07/diagnosing-and-resolving.html, чтобы понять фрагмент кода, который может вызвать ошибку

0 голосов
/ 10 января 2011

java.lang.Error javadoc:

Ошибка - это подкласс Throwable , который указывает на серьезные проблемы, которые разумное приложение не должно пытаться отловить . Большинство таких ошибок являются ненормальными условиями. Ошибка ThreadDeath, хотя и является «нормальным» условием, также является подклассом Error, поскольку большинство приложений не должны пытаться ее перехватить. Метод не обязан объявлять в своем предложении throws любые подклассы Error, которые могут быть выброшены во время выполнения метода, но не обнаружены, так как эти ошибки являются ненормальными условиями, которые никогда не должны возникать.

Так что не надо. Попробуйте найти, что не так в логике вашего кода. Это исключение возникает очень часто из-за бесконечной рекурсии.

...