最终不抛出堆栈溢出异常

12
据我所知,这段代码应该会抛出StackOverflowError异常,但它并没有抛出。可能的原因是什么?
public class SimpleFile {

    public static void main(String[] args) {
        System.out.println("main");

        try{
        SimpleFile.main(args);
            }

        catch(Exception e){
            System.out.println("Catch");
        }

        finally{
            SimpleFile.main(args);
        }
    }

}

6
我猜这是因为main方法是静态的,所以它不是在堆栈上创建,而是在堆的PermGen区域创建。因此,每次调用它时,先前的实例都会丢失。如果您在main方法后立即放置此代码 new SimpleFile().abc() 并创建方法 void abc() {abc(); },那么肯定会抛出stack overflow异常,因为非静态方法是在堆栈上创建的。 - Aamir
@3kings这没有意义。你是在说像 void foo() {System.out.println("foo"); foo(); } 这样的简单方法不会抛出堆栈溢出吗? - eis
@Aamir那不就是一个答案吗? - eis
@eis 我不确定这是否是原因。 - Aamir
@Aamir 我猜不是 `public class A {public static void main(String[] args) { A.test(); } public static void test() { A.main(null); }}` - SpringLearner
5个回答

5

错误不是一个异常,所以捕获任何异常都无法捕获StackOverflowError。

因此,让我们从修复“明显的错误”开始 - (如本答案后面所述,此代码不可取)

    catch(Throwable e){
        System.out.println("Catch");
    }

如果你进行这个更改,你会发现代码仍然没有打印出来。但这并不是不打印的原因...
捕获任何错误(包括StackOverflowError)都是非常不鼓励的。但在这里,你不仅捕获了一个,而是在堆栈顶部捕获了一个。即使在没有上面的更改的情况下,finally块也可以有效地捕获错误。
当堆栈已满并尝试添加更多内容时,就会发生StackOverflowError。所以在捕获错误时,堆栈仍然已满。 因为堆栈已满,所以您无法调用任何方法(甚至无法打印到控制台)。 因此,在成功打印之前,第二个StackOverflowError在catch中被抛出。
其结果是:
  1. 捕获错误
  2. 尝试打印错误
  3. 由于无法打印而导致另一个错误
  4. 调用finally,因为finally始终会被调用。
  5. 由于无法调用main而导致另一个错误
  6. 级联错误返回到上一次调用,遇到相同的错误。
关键在于最终它将开始打印某些内容。但是,调用print使用了大量的堆栈空间,你的代码将不得不通过上述点进行递归和错误处理,很长时间后才能释放足够的堆栈空间来打印。根据Holger的评论,对于需要println堆栈帧的main堆栈帧数量很接近50,使用Oracle的Java 8。
250 = 1,125,899,906,842,624
这就是为什么不能捕获错误的原因。
只有很少几个借口可以让你违反这个规则,并且你已经亲身体验了如果违反这个规则会发生什么。

没错 - 但是代码不应该进入finally块,然后在尝试再次调用SimpleFile.main(args);时抛出另一个StackOverflowError吗? - matt freake
是的,这正是我现在正在调查的问题。看起来有点像一个错误。也许是优化器的问题。 - Philip Couling
我明白了!捕获Throwable是不好的。在堆栈溢出异常发生时捕获它非常糟糕。我已经更新了我的答案。 - Philip Couling
正如许多人发现的那样,仅仅将 catch (Exception e) 更改为 catch (Throwable e) 并不能解决问题。这似乎是 OP 代码的问题,但实际上不仅仅是这个问题。代码更改在我的编辑中丢失了。我可以重新添加它以使答案更清晰。 - Philip Couling
1
现在它是可以理解的了。您可以补充说明println需要比main方法更多的堆栈大小,因此在第一次出现StackOverflowError和第一次打印Catched之间,它将执行2ⁿmain调用,其中n由平坦的main和完整的print操作所需的堆栈大小差异确定。使用Oracle的Java 8,在这里n似乎接近50... - Holger

3

实际上,你遇到了java.lang.Stackoverflow问题

你可以运行以下示例代码:

public class SimpleFile {
    public static void main(String[] args) {
       System.out.println("main ");
       try{
          SimpleFile.main(args);
       }finally{
          try{
             SimpleFile.main(args);
          }catch(Error e2){
             System.out.println("finally");
             throw e2;
          }
       }
   }
}

PS

更多细节:您的程序输出了很多main消息,然后第一次收到堆栈溢出错误并进入最终块。这意味着您减小了堆栈大小,现在可以调用某些内容。但是您在最终块中调用自身,再次发生堆栈溢出。对我来说最令人惊讶的是不稳定的输出:

 main 
 main main finally
 main 
 main main finallyfinallyfinally
 main 
 main 

3

首先,您有一个不捕获Errorcatch子句:

catch(Exception e){
    System.out.println("Catch");
}

由于Error不是Exception,因此它无法捕获StackOverflowError,并且打印语句不会被执行。如果未捕获Error,则线程的默认处理程序将打印其堆栈跟踪(如果线程到达该点)。但您还有另一个子句:

finally{
    SimpleFile.main(args);
}
finally语句块中的代码将始终在try块完成时执行,无论是正常完成还是异常完成。由于您的try块包含无限递归,因此它永远不会正常完成。
在异常情况下,也就是当抛出StackOverflowError时,finally操作将再次进入无限递归状态,可能最终会再次失败并产生StackOverflowError,但由于它具有相同的finally块,因此它也将再次进入无限递归。
您的程序基本上是说“执行一个无限递归,然后再执行另一个无限递归”。请注意,通过打印"main",您无法区分程序是否在主要的无限递归中运行还是从finally块触发的无限递归(除非在println执行之间发生了堆栈溢出,这样可能会缺少换行符)。
因此,如果我们假设特定的JVM具有1000个嵌套调用的限制,则您的程序将执行2¹⁰⁰⁰个调用您的main方法的操作(计算)。由于您的main方法实际上什么也不做,优化器可以省略即使是这么多调用,但是这种优化也意味着所需的堆栈大小消失了,因此可以进行更高数量的递归调用。只有强制执行有意限制支持的递归调用数量的JVM实现,而与实际需要的堆栈空间无关,才能强制执行此程序以终止。
但是请注意,在无限递归的情况下,没有保证会得到StackOverflowError。从理论上讲,具有无限堆栈空间的JVM将是有效的实现。这意味着JVM实现递归代码运行而不需要额外堆栈空间的实际优化也是有效的。
因此,对于像Oracle的JVM这样的典型实现,您的程序几乎不可能报告StackOverflowError。它们确实会发生,但被后续在finally块中的递归所掩盖,因此从未报告。

我喜欢这个理论的探讨。但我并不太喜欢基于无限的假设情况,因为它们往往会让你误以为在有限的情况下也是可能的。即使有一个无限堆栈的JVM,在内存或交换空间耗尽时仍然会崩溃(希望报告一个“StackOverflowError”)。 - Philip Couling
@couling:具有无限堆栈空间的JVM也可能具有所需的无限RAM。这种东西不存在并不重要。它只是一个心理模型,用于解释JVM可以将此递归转换为无限循环,永远打印“main”而不会抛出异常。这样的转换存在。它被称为“尾调用优化”,虽然Oracle的JVM没有它,但其他JVM可能会有。 - Holger
@couling:不完全是这样。这个轮盘策略保证在完成的情况下赢得胜利。但它不能保证永远完成...关于堆栈跟踪,JVM可以简单地计算递归次数,并根据需要重复堆栈中的相关条目。请注意,Oracle当前的实现已经对堆栈跟踪条目数量有限制,在某些情况下,即使没有尾调用优化,它也已经无法提供堆栈跟踪。 - Holger
@couling:你正在改变定义。如果你有无限的钱、时间,以及没有赌注限制,那么轮盘策略是可行的。一旦你面对这些限制与现实的冲突,这个策略就不起作用(因此,不值得讨论)。正如所说,Oracle的JVM已经对堆栈跟踪设置了限制(1024),因此如果计数器能够保存一个数字达到这个范围,那么计数解决方案将与其兼容,这并不过分乐观。如果递归更深入,您将失去报告精确数目的能力,但这与今天的JVM没有区别。 - Holger
“一旦你面对这些限制与现实的冲突,这种策略就不再奏效。” 这正是我想表达的观点。我们似乎现在已经完全同意了。我的第一个评论是:“我不太喜欢基于无限的假设情况。它们往往会让你误以为某些有限的情况是可能的,但实际上并不是。” - Philip Couling

0

我对你的代码进行了一些修改并进行了一些测试。 我仍然无法回答你的问题。 确定是 finally 块导致了代码整体的奇怪行为。

public class SimpleFile {

    static int i = 0;

    public static void main(String[] args) {
        int c = i++;
        System.out.println("main" + i);

        try {
            SimpleFile.main(args);
        }

        catch (Throwable e) {
            System.out.println("Catch" + e);
        }

        finally {
            if (i < 30945) {
                System.out.println("finally" + c);
                SimpleFile.main(args);
            }
        }
    }
}

输出结果为... 我只展示最后几行:

main30941
main30942finally30940
main30943finally30927
main30944
main30945
main30946
main30947Catchjava.lang.StackOverflowError

我想证明即使静态方法也会递归调用导致StackOverflowError。而且,由于你正在捕获永远不会捕获错误的异常。

请参见递归调用主函数时StackOverflowError问题


-1

静态方法是永久的,它不会存储在堆栈中,而是存储在堆中。你编写的代码只是从堆中反复调用相同的代码,因此不会抛出StackOverFlowError。此外,System.out.println("main");内部的字符串存储在相同的位置,即永久位置。即使你反复调用代码,也会使用相同的字符串对象,不会填充堆栈。

我从以下链接中得到了这个解释:

http://www.oracle.com/technetwork/java/javase/memleaks-137499.html#gbyuu

3.1.2 详细信息:PermGen space 详细信息 PermGen space 表示永久代已满。永久代是堆中存储类和方法对象的区域。如果应用程序加载了大量的类,则可能需要使用 -XX:MaxPermSize 选项增加永久代的大小。
Interned java.lang.String 对象也存储在永久代中。java.lang.String 类维护一个字符串池。当调用 intern 方法时,该方法会检查池中是否已经存在相等的字符串。如果有,则 intern 方法返回它;否则将字符串添加到池中。更精确地说,java.lang.String.intern 方法用于获取字符串的规范表示形式;结果是对同一类实例的引用,如果该字符串出现为文字,则返回该引用。如果应用程序内部化了大量字符串,则可能需要从其默认设置中增加永久代的大小。
当发生此类错误时,打印的堆栈跟踪顶部可能会出现文本 String.intern 或 ClassLoader.defineClass。
jmap -permgen 命令打印永久代中对象的统计信息,包括关于内部化字符串实例的信息。

2
静态方法不会导致 StackOverFlowError 是不正确的说法。 - awsome
1
你正在混淆用于存储程序代码的内存和用于存储“堆栈帧”的内存。无论方法是否为静态,对它的调用都必须生成堆栈帧,否则Java将无法跟踪递归了多少次。 - Philip Couling
@couling:在尾调用优化的情况下,递归调用的数量可能会丢失,但是Oracle的JVM不执行这种类型的优化。尽管如此,可能递归的次数取决于方法的优化状态,但您是正确的,这与方法是否为“静态”无关。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接