如何在Java中处理StackOverflowError?

时间:2009-06-04 16:24:51

标签: java exception java-ee stack-overflow

如何在Java中处理StackOverflowError

14 个答案:

答案 0 :(得分:17)

你可能会进行一些无限递归。

即。一种自我调用的方法

public void sillyMethod()
{
    sillyMethod();
}

解决这个问题的方法是修复代码,以便递归终止而不是永远继续。

答案 1 :(得分:16)

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

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

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...");
    }
}

但这很可能是一个坏主意,除非你确切知道自己在做什么。

答案 2 :(得分:13)

查看Raymond Chen的帖子When debugging a stack overflow, you want to focus on the repeating recursive part。提取物:

  

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

     

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

     

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

     

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

答案 3 :(得分:7)

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

答案 4 :(得分:4)

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

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

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

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

理想的做法是永远不要首先分配那些无效的程序或其他任何东西,这正是尾部调用消除所能完成的。使用Java提供的功能,我们可以做的就是正常运行代码,只在我们用完堆栈时才进行这些无效的程序。现在我们仍然分配那些无用的帧,但我们在堆栈而不是堆上这样做,并且批量释放它们,我们的调用也是正常的直接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())); 
    }
}

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

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

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

答案 5 :(得分:1)

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

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

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

答案 6 :(得分:1)

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

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

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

答案 7 :(得分:0)

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

如果它不是一个bug,你需要添加一个计数器或其他一些机制来阻止递归,直到递归过深导致堆栈溢出。

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

答案 8 :(得分:0)

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

答案 9 :(得分:0)

简单,

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

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

答案 10 :(得分:0)

java.lang.Error javadoc:

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

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

答案 11 :(得分:0)

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

答案 12 :(得分:0)

StackOverFlow错误-当您在java中创建方法时,将在堆栈内存中分配一定大小的内存分配。如果您在无限循环内创建一个方法,则内存分配将被创建n次。当内存分配超过时,将发生错误。该错误称为StackOverFlow错误。

如果要避免此错误,请从一开始就考虑堆栈内存大小。

答案 13 :(得分:-1)

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