Как я могу обрабатывать StackOverflowError в Java?
Как я могу обработать StackOverflowError в Java?
Ответы (14)
Я не уверен, что вы имеете в виду под "ручкой".
Вы, конечно, можете поймать эту ошибку:
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...");
}
}
но это, скорее всего, плохая идея, если вы точно не знаете, что делаете.
Вероятно, у вас есть какая-то бесконечная рекурсия.
т.е. метод, который вызывает себя снова и снова
public void sillyMethod()
{
sillyMethod();
}
Один из способов справиться с этим - исправить ваш код, чтобы рекурсия прекращалась, а не продолжалась вечно.
Взгляните на запись Раймонда Чена При отладке переполнения стека следует сосредоточиться на повторяющейся рекурсивной части. Выдержка:
Если вы начнете рыскать по базе данных отслеживания дефектов, пытаясь понять, является ли это известной проблемой или нет, поиск основных функций в стеке вряд ли найдет что-нибудь интересное. Это связано с тем, что переполнение стека имеет тенденцию происходить в случайной точке рекурсии; каждое переполнение стека внешне отличается от любого другого, даже если это одно и то же переполнение стека.
Предположим, вы поете песню Frère Jacques, за исключением того, что вы поете каждый куплет на несколько тонов выше, чем предыдущий. В конце концов, вы достигнете вершины своего певческого диапазона, и именно то, где это произойдет, зависит от того, где ваш вокальный предел совпадает с мелодией. В мелодии каждая из первых трех нот является новой рекордной высотой (т. е. эти ноты выше, чем любая другая нота, спетая до сих пор), и новые рекордные высоты появляются в трех нотах третьего такта, а последняя рекордная высота в вторая нота пятого такта.
Если бы мелодия представляла использование стека программой, переполнение стека могло бы произойти в любом из этих пяти мест выполнения программы. Другими словами, одна и та же скрытая рекурсия (музыкально представленная все более высоким исполнением мелодии) может проявляться пятью различными способами. Рекурсия в этой аналогии была довольно быстрой, всего восемь тактов до повторения цикла. В реальной жизни цикл может быть довольно длинным, что приводит к десяткам потенциальных точек, где может проявиться переполнение стека.
Если вы столкнулись с переполнением стека, то вы хотите игнорировать вершину стека, поскольку это просто сосредоточение на конкретной ноте, которая выходит за пределы вашего вокального диапазона. Вы действительно хотите найти всю мелодию, так как это то, что является общим для всех переполнений стека с одной и той же основной причиной.
Возможно, вы захотите узнать, поддерживается ли параметр «-Xss» вашей JVM. Если это так, вы можете попробовать установить для него значение 512 КБ (по умолчанию 256 КБ в 32-разрядных Windows и Unix) и посмотреть, даст ли это что-нибудь (кроме того, что вы будете сидеть дольше до исключения StackOverflowException). Обратите внимание, что это параметр для каждого потока, поэтому, если у вас запущено много потоков, вы также можете увеличить параметры кучи.
Правильный ответ тот, который уже дан. Вероятно, вы либо а) имеете ошибку в своем коде, приводящую к бесконечной рекурсии, которую обычно довольно легко диагностировать и исправить, либо б) имеете код, который может привести к очень глубоким рекурсиям, например, рекурсивному обходу несбалансированного двоичного дерева. В последнем случае вам нужно изменить свой код, чтобы не размещать информацию в стеке (т. е. не рекурсивно), а вместо этого размещать ее в куче.
Например, для несбалансированного обхода дерева вы можете сохранить узлы, которые необходимо будет повторно посетить, в структуре данных стека. Для обхода по порядку вы будете зацикливаться на левых ветвях, нажимая каждый узел по мере его посещения, пока не нажмете лист, который вы обработаете, затем вытащите узел из вершины стека, обработайте его, затем перезапустите свой цикл с помощью правый дочерний элемент (просто установив переменную цикла в правильный узел). Это будет использовать постоянный объем стека, перемещая все, что было в стеке, в кучу в структуре данных стека. Куча обычно намного больше, чем стек.
В качестве чего-то, что обычно является очень плохой идеей, но необходимо в случаях, когда использование памяти чрезвычайно ограничено, вы можете использовать реверсирование указателя. В этом методе вы кодируете стек в структуру, которую вы проходите, и, повторно используя ссылки, которые вы проходите, вы можете сделать это без дополнительной памяти или со значительно меньшим объемом дополнительной памяти. Используя приведенный выше пример, вместо того, чтобы перемещать узлы во время цикла, нам просто нужно помнить нашего непосредственного родителя, и на каждой итерации мы устанавливаем ссылку, по которой мы прошли, на текущий родитель, а затем текущий родитель на узел, который мы покидаем. Когда мы добираемся до листа, мы обрабатываем его, затем переходим к нашему родителю, и тогда у нас возникает головоломка. Мы не знаем, следует ли исправить левую ветвь, обработать этот узел и продолжить с правой ветвью, или исправить правую ветвь и перейти к нашему родителю. Поэтому нам нужно выделить дополнительный бит информации по мере выполнения итерации. Как правило, для низкоуровневых реализаций этого метода этот бит будет храниться в самом указателе, что приводит к отсутствию дополнительной памяти и постоянной памяти в целом. Это не вариант в Java, но может быть возможно убрать этот бит в поля, используемые для других целей. В худшем случае это все еще по крайней мере в 32 или 64 раза меньше необходимого объема памяти. Конечно, в этом алгоритме очень легко ошибиться, так как он приводит к совершенно запутанным результатам, а параллелизм может привести к полному хаосу. Таким образом, это почти никогда не стоит кошмара обслуживания, за исключением случаев, когда выделение памяти несостоятельно. Типичным примером является сборщик мусора, в котором распространены подобные алгоритмы.
Однако я действительно хотел поговорить о том, когда вы можете захотеть обработать StackOverflowError. А именно, чтобы обеспечить устранение хвостовых вызовов на JVM. Один из подходов заключается в использовании стиля трамплина, в котором вместо выполнения хвостового вызова вы возвращаете нулевой объект процедуры или, если вы просто возвращаете значение, вы возвращаете его. [Примечание: для этого требуются какие-то средства сказать, что функция возвращает либо A, либо B. В Java, вероятно, самый простой способ сделать это — обычно возвращать один тип и выбрасывать другой как исключение.] Затем всякий раз, когда вы вызываете метод, вы нужно выполнить цикл while, вызывающий нульарные процедуры (которые сами вернут либо нульарную процедуру, либо значение), пока вы не получите значение. Бесконечный цикл станет циклом while, который постоянно заставляет объекты процедур возвращать объекты процедур. Преимущество стиля трамплина состоит в том, что он использует стек только на постоянный множитель больше, чем вы использовали бы с реализацией, которая должным образом устранила все хвостовые вызовы, он использует обычный стек Java для не-хвостовых вызовов, простой перевод и только увеличивает код с (утомительным) постоянным коэффициентом. Недостатком является то, что вы выделяете объект при каждом вызове метода (который сразу же становится мусором), и использование этих объектов включает пару косвенных вызовов на хвостовой вызов.
В идеале было бы никогда не выделять эти пустые процедуры или что-то еще в первую очередь, что и было бы достигнуто устранением хвостового вызова. Однако, работая с тем, что предоставляет Java, мы могли бы запустить код как обычно и выполнять эти нулевые процедуры только тогда, когда у нас заканчивается стек. Теперь мы по-прежнему выделяем эти бесполезные кадры, но делаем это в стеке, а не в куче, и освобождаем их массово, кроме того, наши вызовы являются обычными прямыми вызовами Java. Самый простой способ описать это преобразование — сначала переписать все методы с несколькими операторами вызова в методы с двумя операторами вызова, т. е. fgh() { f(); грамм(); час(); } становится fgh() { f(); гх(); } и 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, а не людьми.
Я думаю, вы не можете - или, по крайней мере, это зависит от используемой вами jvm. Переполнение стека означает, что у вас нет места для хранения локальных переменных и адресов возврата. Если ваш jvm выполняет какую-либо форму компиляции, у вас также есть переполнение стека в jvm, и это означает, что вы не можете его обработать или поймать. JVM должен завершиться.
Можно было бы создать jvm, допускающий такое поведение, но это было бы медленно.
Я не проверял поведение с jvm, но в .net вы просто не можете справиться с переполнением стека. Даже попытка поймать не поможет. Поскольку java и .net основаны на одной и той же концепции (виртуальные машины с jit), я подозреваю, что java будет вести себя одинаково. Наличие исключения stackoverflow в .NET предполагает, что может быть какая-то виртуальная машина, которая позволяет программе ее перехватывать, хотя обычно этого не происходит.
Большинство шансов получить StackOverflowError связаны с использованием [длинных/бесконечных] рекурсий в рекурсивных функциях.
Вы можете избежать рекурсии функций, изменив дизайн своего приложения, чтобы использовать стекируемые объекты данных. Существуют шаблоны кодирования для преобразования рекурсивных кодов в итеративные блоки кода. Посмотрите на ответы ниже:
- way-to-go-from-recursion-to-iteration
- can-every-recursion-be-converted-into-iteration
- design-patterns-for-converting-recursive-algorithms-to- итеративные
Таким образом, вы избегаете стека памяти Java для ваших рецессивных вызовов функций, используя свои собственные стеки данных.
В некоторых случаях вы не можете поймать StackOverflowError.
Всякий раз, когда вы пытаетесь, вы столкнетесь с новым. Потому что это виртуальная машина Java. Хорошо найти рекурсивные блоки кода, как сказал Andrew Bullock.
Трассировка стека должна указывать на характер проблемы. При чтении трассировки стека должно быть очевидное зацикливание.
Если это не ошибка, вам нужно добавить счетчик или какой-либо другой механизм, чтобы остановить рекурсию до того, как рекурсия станет настолько глубокой, что вызовет переполнение стека.
Примером этого может быть ситуация, когда вы обрабатываете вложенный XML в модели DOM с рекурсивными вызовами, а XML вложен настолько глубоко, что вызывает переполнение стека вашими вложенными вызовами (маловероятно, но возможно). Однако это должно быть довольно глубокое вложение, чтобы вызвать переполнение стека.
Как упоминалось многими в этой ветке, распространенной причиной этого является рекурсивный вызов метода, который не завершается. По возможности избегайте переполнения стека, и если вы это делаете при тестировании, в большинстве случаев это следует рассматривать как серьезную ошибку. В некоторых случаях вы можете настроить размер стека потоков в Java, чтобы он был больше, чтобы справиться с некоторыми обстоятельствами (большие наборы данных, управляемые в локальном хранилище стека, длинные рекурсивные вызовы), но это увеличит общий объем памяти, что может привести к проблемам в числе потоков, доступных в виртуальной машине. Как правило, если вы получаете это исключение, поток и любые локальные данные в этом потоке должны считаться испорченными и не использоваться (т.е. подозрительными и, возможно, поврежденными).
Простой,
Посмотрите на трассировку стека, которую создает StackOverflowError, чтобы вы знали, где в вашем коде она возникает, и используйте ее, чтобы выяснить, как переписать ваш код, чтобы он не вызывал себя рекурсивно (вероятная причина вашей ошибки), чтобы он не работал. не случится снова.
StackOverflowErrors — это не то, что нужно обрабатывать с помощью предложения try...catch, но оно указывает на основной недостаток в логике вашего кода, который вы должны исправить.
java.lang.Ошибка javadoc:
Ошибка — это подкласс Throwable, указывающий на серьезные проблемы, которые разумное приложение не должно пытаться отловить. Большинство таких ошибок являются ненормальными условиями. Ошибка ThreadDeath, хотя и является «нормальным» состоянием, также является подклассом ошибки, поскольку большинству приложений не следует пытаться ее перехватить. Метод не обязан объявлять в своем предложении throws какие-либо подклассы Error, которые могут быть выброшены во время выполнения метода, но не перехвачены, поскольку эти ошибки являются ненормальными условиями, которые никогда не должны возникать.
Так что не надо. Попробуйте найти, что не так в логике вашего кода. Это исключение возникает очень часто из-за бесконечной рекурсии.
Ошибка StackOverFlow - при создании метода на Java в момент времени в памяти стека будет выделен некоторый объем памяти. Если вы создадите метод внутри бесконечного цикла, то выделение памяти будет создано «n» раз. При превышении лимита на выделение памяти возникает ошибка. Эта ошибка называется ошибкой StackOverFlow.
Если вы хотите избежать этой ошибки, учитывайте размер стековой памяти во время реализации с самого начала.
/*
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, чтобы понять фрагмент кода, который может вызвать ошибку