如何在Java中处理StackOverflowError?

如何在Java中处理StackOverflowError

我不确定你对“手柄”的意思。

你当然可以发现这个错误:

 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(); } 

处理此问题的方法是修复代码,以便递归终止而不是永远继续。

看一下Raymond Chen的post在调试堆栈溢出时,你想要专注于重复的递归部分 。 提取物:

如果你去寻找缺陷跟踪数据库试图查看这是否是一个已知问题,搜索堆栈顶部函数不太可能找到任何有趣的东西。 这是因为堆栈溢出往往发生在递归中的随机点; 即使堆栈溢出相同,每个堆栈溢出看起来也与其他堆栈溢出不同。

假设你正在唱“ FrèreJacques”这首歌,只不过你唱的歌曲比前一首歌要高几个音。 最终,你将达到你的演唱范围的顶部,并且恰好发生的地方取决于你的声音限制与旋律的对比。 在旋律中,前三个音符都是新的“记录高”(即,音符高于到目前为止所唱的任何其他音符),并且在第三个音符的三个音符中出现新的唱片高音,并且最终记录在第五项措施的第二个注释中很高。

如果旋律表示程序的堆栈使用情况,则程序执行中的这五个位置中的任何一个都可能发生堆栈溢出。 换句话说,相同的潜在失控递归(音乐上由旋律的更高版本表示)可以以五种不同的方式表现出来。 这个类比中的“递归”相当快,在循环重复之前只有八个小节。 在现实生活中,循环可能会很长,导致堆栈溢出可能出现的数十个潜在点。

如果您遇到堆栈溢出,那么,您想要忽略堆栈的顶部,因为这只关注超出您的声音范围的特定音符。 你真的想要找到整个旋律,因为这是所有堆栈溢出的共同点,具有相同的根本原因。

您可能希望查看JVM是否支持“-Xss”选项。 如果是这样,你可能想尝试将其设置为512k(在32位Windows和Unix下默认为256k)并查看是否有任何效果(除了让你坐得更久,直到你的StackOverflowException)。 请注意,这是一个每线程设置,所以如果你有很multithreading在运行,你也可能想要提高你的堆设置。

正确答案是已经给出的答案。 您可能a)在您的代码中有一个错误导致无限递归,这通常很容易诊断和修复,或者b)具有可导致非常深度递归的代码,例如递归遍历不平衡二叉树。 在后一种情况下,您需要更改代码以不在堆栈上分配信息(即不递归),而是将其分配到堆中。

例如,对于非平衡树遍历,您可以存储需要在堆栈数据结构中重新访问的节点。 对于顺序遍历,您将在访问它时循环向左推动每个节点,直到您点击要处理的叶子,然后从堆栈顶部弹出一个节点,处理它,然后重新启动循环。右子(通过将循环变量设置为正确的节点。)这将通过将堆栈上的所有内容移动到堆栈数据结构中的堆来使用恒定量的堆栈。 堆通常比堆栈丰富得多。

作为一个通常非常糟糕的想法,但在内存使用受到极大限制的情况下是必要的,你可以使用指针反转。 在这种技术中,您将堆栈编码为您正在遍历的结构,并且通过重用您正在遍历的链接,您可以在没有或显着减少额外内存的情况下执行此操作。 使用上面的示例,我们只需要记住我们的直接父节点,而不是在循环时推送节点,并且在每次迭代时,我们设置我们遍历到当前父节点的链接,然后将当前父节点设置为我们要离开的节点。 当我们得到一片叶子,我们处理它,然后去我们的父母,然后我们有一个难题。 我们不知道是否要更正左侧分支,处理此节点,继续使用右侧分支,或者更正右侧分支并转到我们的父级。 所以我们需要在迭代时分配额外的信息。 通常,对于该技术的低级实现,该位将存储在指针本身中,导致没有额外的存储器和整体的恒定存储器。 这不是Java中的一个选项,但有可能在用于其他事物的字段中松开这一点。 在最坏的情况下,这仍然是所需内存量减少至少32或64倍。 当然,这种算法很容易出错,完全混淆了结果,并且会对并发性造成严重破坏。 所以除了分配内存不可靠之外,几乎不值得维护噩梦。 典型的例子是垃圾收集器,这种算法很常见。

但是,我真正想谈的是你何时想要处理StackOverflowError。 即在JVM上提供尾调用消除。 一种方法是使用trampoline样式,而不是执行尾部调用,而是返回一个nullary过程对象,或者如果您只是返回一个值,则返回该值。 [注意:这需要一些方法来表示函数返回A或B.在Java中,最轻的方法可能是正常返回一种类型并将另一种类型作为exception抛出。]然后每当你调用一个方法时,你需要做一个while循环调用nullary过程(它们自己会返回一个nullary过程或值),直到你得到一个值。 无限循环将成为一个while循环,它不断强制返回过程对象的过程对象。 trampoline风格的好处是它只使用一个常量因子比你使用正确消除所有尾调用的实现更多的堆栈,它使用普通的Java堆栈进行非尾调用,转换简单,并且它只增加了代码由(繁琐)常数因子组成。 缺点是你在每个方法调用上分配一个对象(它将立即变成垃圾),并且消耗这些对象涉及每个尾调用的几个间接调用。

理想的做法是永远不要在第一时间分配那些无效程序或其他任何东西,这正是尾部调用消除所能完成的。 使用Java提供的function,我们可以做的就是正常运行代码,只有当我们用完堆栈时才会生成这些无效的程序。 现在我们仍然分配那些无用的帧,但是我们在堆栈而不是堆上这样做,并且批量释放它们,我们的调用也是正常的直接Java调用。 描述此转换的最简单方法是首先将所有多调用语句方法重写为具有两个调用语句的方法,即fgh(){f(); G(); H(); 变成fgh(){f(); GH(); }和gh(){g(); H(); }。 为简单起见,我假设所有方法都以尾调用结束,可以通过将方法的其余部分打包到单独的方法来安排,但在实践中,您需要直接处理这些方法。 在这些转换之后我们有三种情况,要么一个方法有零调用,在这种情况下没有任何事情要做,或者它有一个(尾部)调用,在这种情况下我们将它包装在try-catch块中我们将用于两个呼叫案例中的尾部呼叫。 最后,它可能有两个调用,一个非尾调用和一个尾调用,在这种情况下,我们应用下面的示例说明的转换(使用C#的lambda表示法,可以很容易地用一个增长的匿名内部类替换):

 // 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())); } } 

这里的主要好处是如果没有抛出exception,这与我们刚开始安装一些额外的exception处理程序时的代码相同。 由于尾调用(h()调用)不处理Bounceexception,因此该exception将通过它们从堆栈中展开那些(不必要的)帧。 非尾调用捕获Bounceexception并在添加剩余代码时重新抛出它们。 这会将堆栈一直展开到顶层,消除尾部调用帧,但记住nullary过程中的非尾调用帧。 当我们最终在顶层的Bounceexception中执行该过程时,我们将重新创建所有非尾调用帧。 此时,如果我们立即再次耗尽堆栈,那么,因为我们不重新安装StackOverflowError处理程序,所以它将根据需要不被捕获,因为我们真的没有堆栈。 如果我们进一步了解,将根据需要安装新的StackOverflowError。 此外,如果我们确实取得了进展,但是再次耗尽堆栈,则重新展开我们已经解开的帧没有任何好处,因此我们安装新的顶级处理程序,以便堆栈只能解开它们。

这种方法的最大问题是你可能想要调用普通的Java方法,并且你可能会有任意小的堆栈空间,所以它们可能有足够的空间来启动但没有完成,你无法在中间。 至少有两种解决方案。 第一种是将所有这些工作发送到一个单独的线程,该线程将拥有自己的堆栈。 这非常有效且非常简单,并且不会引入任何并发性(除非您希望它。)另一个选项是在调用任何普通的Java方法之前通过简单地在它们之前立即抛出StackOverflowError来故意展开堆栈。 如果你恢复时它仍然没有堆栈空间,那么你就开始搞砸了。

也可以采用类似的方法及时进行延续。 不幸的是,这种转换在Java中并不是可以手工完成的,并且可能是C#或Scala等语言的边界。 因此,像这样的转换往往是由针对JVM而不是人的语言完成的。

我想你不能 – 或者它至少取决于你使用的jvm。 堆栈溢出意味着您没有空间存储局部变量并返回地址。 如果你的jvm做某种forms的编译,你也有jvm中的stackoverflow,这意味着你无法处理或捕获它。 jvm必须终止。

可能有一种方法可以创建一个允许这种行为的jvm,但它会很慢。

我没有测试jvm的行为,但在.net中你只是无法处理stackoverflow。 即使尝试捕获也无济于事。 由于java和.net依赖于相同的概念(带有jit的虚拟机),我怀疑java的行为是一样的。 在.NET中存在stackoverflow-exception表明,可能有一些vm确实使程序能够捕获它,但正常情况并非如此。

获得StackOverflowError最大机会是通过在递归函数中使用[long / infinite]递归。

您可以通过更改应用程序设计以使用可堆叠数据对象来避免函数递归。 存在将递归代码转换为迭代代码块的编码模式。 看看下面的回答:

  • 这样,到去-从递归到迭代
  • jar每递归待转化-进入-迭代
  • 设计图案换转换递归的算法对迭代酮

因此,通过使用您自己的数据堆栈,您可以避免Java通过隐性函数调用进行内存堆叠。

堆栈跟踪应指出问题的性质。 读取堆栈跟踪时应该有一些明显的循环。

如果它不是一个bug,你需要添加一个计数器或一些其他机制来在递归过程之前停止递归,这会导致堆栈溢出。

这种情况的一个示例可能是,如果您在DOM模型中使用递归调用处理嵌套XML,并且XML嵌套得如此之深,则会导致堆栈溢出与嵌套调用(不太可能,但可能)。 这必须是相当深的嵌套,以导致堆栈溢出。

正如此线程中的许多人所提到的,导致此问题的常见原因是不终止的递归方法调用。 在可能的情况下避免堆栈溢出,如果你在测试中,你应该在大多数情况下认为这是一个严重的错误。 在某些情况下,您可以将Java中的线程堆栈大小配置为更大以处理某些情况(在本地堆栈存储中管理大型数据集,长递归调用)但这会增加总体内存占用量,从而导致数量问题VM中可用的线程数。 通常,如果您收到此exception,则应将此线程的线程和任何本地数据视为toast而不使用(即怀疑且可能已损坏)。

简单,

查看StackOverflowError产生的堆栈跟踪,以便您知道代码中的位置,并使用它来弄清楚如何重写代码,以便它不会递归调用自身(可能导致错误),因此它赢了“再次发生。

StackOverflowErrors不是需要通过try … catch子句处理的东西,但它指出了代码逻辑中需要修复的基本缺陷。

java.lang.Error javadoc:

Error是Throwable的子类,表示合理的应用程序不应该尝试捕获的严重问题 大多数此类错误都是exception情况。 ThreadDeath错误,虽然是“正常”条件,但也是Error的子类,因为大多数应用程序不应该尝试捕获它。 一个方法不需要在其throws子句中声明在执行方法期间可能抛出但未捕获的任何Error类,因为这些错误是永远不应发生的exception情况。

所以,不要。 尝试找出代码逻辑中的错误。 由于无限递归,这种exception经常发生。

在某些场合,你无法捕获stackoverflower。 每当你尝试,你会遇到新的。 因为是java vm。 找到递归代码块是很好的,比如Andrew Bullock说的 。

 /* 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以了解代码段,这可能会引发错误