【问题标题】:finally not throwing stack overflow exception终于不抛出堆栈溢出异常
【发布时间】:2016-03-07 15:01:41
【问题描述】:

据我所知,这段代码应该抛出 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);
        }
    }

}

【问题讨论】:

  • 我猜是因为 main 是一个静态方法不是在堆栈上创建的,而是在堆的 PermGen 部分创建的,所以每次我们调用它时,前一个实例都会丢失,如果你把这个代码 new SimpleFile().abc() 紧跟在后面你的 main 方法和 create 方法 void abc() {abc(); } 然后肯定会抛出 stackoverflow 异常,因为非静态方法是在堆栈上创建的。
  • @3kings 这毫无意义。你是说像void foo() {System.out.println("foo"); foo(); } 这样的简单方法不会引发堆栈溢出吗?
  • @Aamir 这不是答案吗?
  • @eis 我不确定这是否是原因
  • @Aamir 我猜不是public class A { public static void main(String[] args) { A.test(); } public static void test() { A.main(null); } }

标签: java exception exception-handling stack-overflow


【解决方案1】:

Error 不是Exception。所以捕获任何异常都不会捕获 StackOverflowError。

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

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

如果您进行此更改,您会发现代码仍然无法打印。但它并没有因为一个非常不同的原因打印......

非常不鼓励捕获任何ERROR(包括StackOverflowError)。但是在这里,您不仅要捕获一个,而且要捕获一个,因为它发生在堆栈的顶部。即使使用您的代码(没有上述更改),finally 块也有效地捕获了错误。

StackOverflowError 发生在堆栈已满并且您尝试向其中添加更多内容时。因此,当您发现错误时,堆栈仍然是满的。您不能调用任何方法(甚至打印到控制台),因为堆栈已满。因此,在成功打印之前,catch 中会抛出第二个 StackOverflowError

这样做的结果是:

  1. 捕获错误
  2. 尝试打印错误
  3. 由于无法打印而导致另一个错误
  4. 调用finally,因为 finally总是被调用。
  5. 导致另一个错误,因为它无法调用main
  6. 将错误级联回遇到相同错误的上一个调用。

这里的关键是最终它会开始打印一些东西。但是对print 的调用会占用大量堆栈空间,并且您的代码将不得不在很长一段时间中重复上述各点并出错,然后才能释放足够的堆栈空间来打印。根据 Holger 对 Oracle 的 Java 8 的评论,将 println 堆栈帧所需的 main 堆栈帧数接近 50。

250 = 1,125,899,906,842,624

这就是为什么你不应该发现错误

只有少数借口可以让你打破这条规则,而且你已经亲身发现如果你真的打破它会出现什么问题。

【讨论】:

  • 没错——但代码不应该进入 finally 块,然后在尝试调用 SimpleFile.main(args); 时抛出另一个 StackOverflowError;再来一次?
  • 是的,这就是我现在正在调查的内容。它看起来有点像一个错误。也许与优化器。
  • 我明白了!捕捉可投掷物很糟糕。在发生堆栈溢出异常时捕获它是非常糟糕的。我已经更新了我的答案。
  • @Holger 正如许多人所发现的那样,仅仅将catch (Exception e) 更改为catch (Throwable e) 不足以解决问题。乍一看,问题在于 OP 的代码是什么,但还有更多的作用。代码更改丢失了我的编辑。我可以把它放回去让答案更清楚。
  • 现在可以理解了。您可能会补充说,printlnmain 方法需要更多的堆栈大小,因此在StackOverflowError 的第一次出现和Catched 的第一次打印之间,它将执行2ⁿ 更多的@987654342 调用@ 其中n 由平面main 所需的堆栈大小与完整的print 操作之差决定。使用 Oracle 的 Java 8,n 似乎接近 50 这里...
【解决方案2】:

其实你有 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 消息,之后你第一次收到堆栈溢出错误并转到 finally 块。这意味着你减少了堆栈大小,现在你可以调用一些东西。但是你在 finally 块中调用自己并再次获得堆栈溢出。最让我惊讶的是输出不稳定:

 main 
 main main finally
 main 
 main main finallyfinallyfinally
 main 
 main 

【讨论】:

    【解决方案3】:

    首先,您有一个 catch 子句,它不会捕获 Errors:

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

    由于Errors 不是Exceptions,这不会捕获StackOverflowErrors 并且打印语句不会被执行。如果 Error 没有被捕获,它的堆栈跟踪将由线程的默认处理程序打印,如果它到达那个点。但是你还有另一个子句:

    finally{
        SimpleFile.main(args);
    }
    

    finally 子句的代码将始终在 try 块完成时执行,无论是正常还是异常。由于您的 try 块包含无限递归,它永远不会正常完成。

    在例外情况下,即当抛出StackOverflowError 时,finally 操作将再次进入无限递归,最终可能再次以StackOverflowError 失败,但由于它具有相同的@987654336 @block,它也会再次进入无限递归。

    您的程序基本上是在说“执行一次无限递归,然后再执行一次无限递归”。请注意,您无法区分 "main" 的打印,该程序是在主无限递归中运行还是在 finally 块触发的程序中运行(除非在 @987654339 之间发生堆栈溢出时可能会丢失换行符@执行)。

    因此,如果我们假设特定 JVM 的嵌套调用限制为 1000,那么您的程序将执行 2¹⁰⁰⁰ 调用 main 方法 (quantification)。由于您的 main 方法实际上什么都不做,优化器甚至可以忽略如此多的调用,但这种优化也意味着所需的堆栈大小消失了,因此,更多的递归调用成为可能。只有一个 JVM 实现对支持的递归调用数量进行有意限制,独立于实际需要的堆栈空间,才能强制该程序永远终止。

    但请注意,在无限递归的情况下,根本无法保证获得StackOverflowError。从理论上讲,具有无限堆栈空间的 JVM 将是一个有效的实现。这意味着,实际上优化递归代码以在不需要额外堆栈空间的情况下运行的 JVM 也是有效的。

    因此,对于像 Oracle 的 JVM 这样的典型实现,您的程序几乎不可能报告StackOverflowError。它们会发生,但会被您在 finally 块中的后续递归所掩盖,因此永远不会被报告。

    【讨论】:

    • 我喜欢这种对理论的探索。我不太喜欢基于无穷大的假设情况。他们有一个租约来欺骗你,让你认为在有限的情况下某些事情是可能的,而实际上并非如此。仅当 RAM 或交换空间用完时,具有无限堆栈的 JVM 仍然会失败(希望报告 StackOverflowError)。
    • @couling:具有无限堆栈空间的 JVM 也可能具有所需的无限 RAM。这样的事情不存在也没关系。这只是一个心理模型来解释,JVM 将这种递归转换为无限循环打印"main" 永远不会抛出是合法的。而这样的转变存在。它被称为“尾调用优化”,虽然 Oracle 的 JVM 没有,但其他人可能有。
    • @couling:不完全是。这种轮盘赌策略保证在它完成的情况下获胜。但它不能保证永远完成......关于堆栈跟踪,JVM 可以简单地计算递归次数并根据需要重复堆栈中的相关条目。请注意,Oracle 当前的实现限制了堆栈跟踪条目的数量,并且在某些情况下,即使没有尾调用优化,它也根本不提供堆栈跟踪。
    • @couling:您正在更改定义。如果您有无限的金钱、无限的时间并且下注没有限制,那么轮盘赌策略就会奏效。一旦你面对现实中的这些限制之一,这个策略不起作用(因此,不值得讨论)。如前所述,Oracle 的 JVM 已经 限制了堆栈跟踪 (1024),因此如果计数器能够保持一个数字,那么计数解决方案将与该限制兼容,这并不太远获取。如果递归更深入,您将失去报告确切数字的能力,但这与今天的 JVM 没有什么不同。
    • “一旦你面对现实中的这些限制之一,这个策略就行不通了” 这正是我要表达的观点。我们现在似乎非常同意。我的第一条评论是“我不太喜欢基于无限的假设情况。他们倾向于诱使您认为有限情况下某些事情是可能的,而实际上并非如此。”
    【解决方案4】:

    我对您的代码进行了一些修改并进行了一些测试。我仍然无法弄清楚你的问题的答案。肯定是 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。而且由于您正在捕获永远不会捕获错误的异常。

    如果 main 被递归调用,请参阅 Call Main Recursively 问题以了解 StackOverflowError

    【讨论】:

      【解决方案5】:

      静态方法是永久性的,它不存储在堆栈中,而是存储在堆中。您编写的代码代码只是从堆中一遍又一遍地调用相同的代码,因此它不会抛出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 方法时,该方法会检查池以查看 如果池中已经有一个相等的字符串。如果有,那么 实习生方法返回它;否则它将字符串添加到池中。在 更准确地说,java.lang.String.intern 方法用于 获取字符串的规范表示;结果是一个 引用相同的类实例,如果那样的话 字符串作为文字出现。如果一个应用程序实习了大量 字符串,永久代可能需要从 其默认设置。

      当出现此类错误时,文本 String.intern 或 ClassLoader.defineClass 可能出现在堆栈跟踪顶部附近 打印出来的。

      jmap -permgen 命令打印 永久代,包括有关内部化字符串的信息 实例。

      【讨论】:

      • 静态方法不会抛出 StackOverFlowError 是不正确的
      • 您对用于存储程序代码的内存和用于存储“堆栈帧”的内存感到困惑。不管一个方法是否是静态的,对它的调用都必须生成堆栈帧,否则 java 将无法跟踪它已经递归了多少次。
      • @couling:在尾调用优化的情况下,递归调用的数量确实可能会丢失,但是Oracle的JVM不会进行这种优化。不过,可能的递归次数取决于方法的优化状态,但您是对的,这与方法是否为 static 无关。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-27
      • 1970-01-01
      • 2019-07-02
      • 2010-12-07
      • 1970-01-01
      相关资源
      最近更新 更多