Правильный ответ - тот, который уже дан. Вы, вероятно, либо: а) имеете ошибку в своем коде, приводящую к бесконечной рекурсии, которую обычно довольно легко диагностировать и исправлять, либо б) имеете код, который может привести к очень глубоким рекурсиям, например, рекурсивно обходить несбалансированное двоичное дерево. В последнем случае вам нужно изменить свой код, чтобы не размещать информацию в стеке (т.е. не повторять), а вместо этого размещать ее в куче.
Например, для несбалансированного обхода дерева вы можете хранить узлы, которые необходимо будет пересмотреть, в структуре данных стека. Для обхода по порядку вы должны были бы зацикливаться на левых ветвях, толкая каждый узел, когда вы посещали его, до тех пор, пока вы не дойдете до листа, который вы будете обрабатывать, затем вытолкнуть узел из верхней части стека, обработать его, а затем перезапустить цикл с правый дочерний элемент (просто установив переменную цикла в правый узел.) Это будет использовать постоянный объем стека, перемещая все, что было в стеке, в кучу в структуре данных стека. Куча, как правило, гораздо больше, чем стек.
Как то, что обычно является чрезвычайно плохой идеей, но необходимо в случаях, когда использование памяти чрезвычайно ограничено, вы можете использовать обращение указателя. В этом методе вы кодируете стек в структуру, которую вы пересекаете, и, повторно используя проходы, которые вы пересекаете, вы можете сделать это без или значительно меньше дополнительной памяти. Используя приведенный выше пример, вместо того, чтобы выдвигать узлы, когда мы выполняем цикл, нам просто нужно запомнить нашего непосредственного родителя, и на каждой итерации мы устанавливаем ссылку, по которой мы перешли, на текущего родителя, а затем текущего родителя на узел, который мы покидаем. Когда мы добираемся до листа, мы обрабатываем его, затем идем к нашему родителю, и тогда у нас есть загадка. Мы не знаем, исправить ли левую ветвь, обработать этот узел и продолжить с правой веткой, или исправить правую ветку и перейти к нашему родителю. Поэтому нам нужно выделить дополнительный бит информации во время итерации. Как правило, для низкоуровневых реализаций этого метода этот бит будет храниться в самом указателе, что приведет к отсутствию дополнительной памяти и общей постоянной памяти. Это не вариант в 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, а не людьми.