16

我如何StackOverflowError在 Java 中处理?

4

14 回答 14

21

我不确定您所说的“句柄”是什么意思。

您当然可以发现该错误:

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

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

于 2009-06-04T16:51:46.817 回答
18

您可能正在进行一些无限递归。

即一种一遍又一遍地调用自己的方法

public void sillyMethod()
{
    sillyMethod();
}

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

于 2009-06-04T16:31:34.823 回答
13

看看Raymond Chen的帖子当调试堆栈溢出时,你想专注于重复递归部分。提取物:

如果您通过缺陷跟踪数据库寻找这是否是一个已知问题,则搜索堆栈中的顶级函数不太可能找到任何有趣的东西。这是因为堆栈溢出往往发生在递归中的随机点。每个堆栈溢出看起来都与其他堆栈溢出表面上不同,即使它们是相同的堆栈溢出。

假设你正在唱这首歌Frère Jacques,只是你唱的每一节都比前一节高几个音。最终,你会达到你的歌唱范围的顶端,而具体发生在哪里取决于你的声音极限与旋律的对齐位置。在旋律中,前三个音符分别是一个新的“记录高”(即音符比迄今为止所唱的任何一个音符都高),新的记录高出现在第三小节的三个音符中,并成为最终记录第五小节的第二个音符高。

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

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

于 2009-06-04T16:54:33.270 回答
7

您可能想查看您的 JVM 是否支持“-Xss”选项。如果是这样,您可能想尝试将其设置为 512k 的值(在 32 位 Windows 和 Unix 下默认为 256k)并查看是否有任何作用(除了让您坐得更久直到您的 StackOverflowException)。请注意,这是针对每个线程的设置,因此如果您有很多线程正在运行,您可能还需要提高堆设置。

于 2009-06-04T17:21:25.310 回答
4

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

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

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

不过,我真正想谈的是,您何时可能想要处理 StackOverflowError。即在JVM上提供尾调用消除。一种方法是使用蹦床样式,而不是执行尾调用,而是返回一个空过程对象,或者如果您只是返回一个值,则返回该值。[注意:这需要某种方式来表示函数返回 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 表示法,它可以很容易地替换为具有一定增长的匿名内部类):或者它有一个(尾)调用,在这种情况下,我们将它包装在一个 try-catch 块中,就像我们在两个调用情况下的尾调用一样。最后,它可能有两个调用,一个非尾调用和一个尾调用,在这种情况下,我们应用示例说明的以下转换(使用 C# 的 lambda 表示法,它可以很容易地替换为具有一定增长的匿名内部类):或者它有一个(尾)调用,在这种情况下,我们将它包装在一个 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 异常并在添加剩余代码的情况下重新抛出它们。这将展开堆栈一直到顶层,消除尾调用帧,但记住空过程中的非尾调用帧。当我们最终在顶层执行 Bounce 异常中的过程时,我们将重新创建所有非尾调用帧。此时,如果我们立即再次用完堆栈,那么,由于我们没有重新安装 StackOverflowError 处理程序,它会按照需要未被捕获,因为我们真的没有堆栈。如果我们再进一步,将适当安装一个新的 StackOverflowError。此外,如果我们确实取得了进展,但再次用完堆栈,重新展开我们已经展开的帧没有任何好处,因此我们安装了新的顶级处理程序,以便堆栈只会展开到它们。

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

也可以做类似的事情来及时进行延续。不幸的是,这种转换在 Java 中并不是真正可以忍受的,并且对于 C# 或 Scala 等语言来说可能是临界的。因此,像这样的转换往往是由针对 JVM 的语言而不是人来完成的。

于 2012-07-05T01:29:14.490 回答
1

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

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

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

于 2009-06-04T16:31:44.420 回答
1

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

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

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

于 2013-06-01T06:09:52.950 回答
1

在某些情况下,您无法捕获 StackOverflowError。

每当你尝试时,你都会遇到一个新的。因为它是Java VM。找到递归代码块是件好事,就像Andrew Bullock 所说的那样。

于 2014-06-24T03:46:10.117 回答
0

堆栈跟踪应表明问题的性质。当您阅读堆栈跟踪时,应该有一些明显的循环。

如果它不是错误,则需要添加一个计数器或其他一些机制来在递归深入到导致堆栈溢出之前停止递归。

例如,如果您正在使用递归调用处理 DOM 模型中的嵌套 XML,并且 XML 嵌套得太深,以至于嵌套调用会导致堆栈溢出(不太可能,但可能)。不过,这必须是非常深的嵌套才能导致堆栈溢出。

于 2009-06-04T16:41:53.397 回答
0

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

于 2009-06-04T16:47:18.693 回答
0

简单的,

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

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

于 2009-06-04T20:18:46.453 回答
0

java.lang.Error javadoc:

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

所以,不要。尝试找出代码逻辑中的问题。由于无限递归,此异常经常发生。

于 2011-01-10T14:42:14.753 回答
0

StackOverFlow 错误 - 当您在 Java 中创建方法时,会在堆栈内存中分配一定大小的内存。如果您在无限循环中创建一个方法,那么将创建“n”次内存分配。当超出内存分配限制时,将发生错误。该错误称为 StackOverFlow 错误。

如果您想避免此错误,请从一开始就考虑实施过程中的堆栈内存大小。

于 2020-01-09T13:52:54.233 回答
-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以了解可能引发错误的代码片段

于 2012-12-04T10:20:03.060 回答