为什么这个方法打印4?

时间:2013-07-24 08:16:30

标签: java jvm stack-overflow

我想知道当您尝试捕获StackOverflowError时会发生什么,并提出以下方法:

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

现在我的问题:

为什么这个方法打印'4'?

我想也许是因为System.out.println()在调用堆栈上需要3个段,但我不知道3号来自哪里。当你查看System.out.println()的源代码(和字节码)时,它通常会导致比3更多的方法调用(因此调用堆栈上的3个段是不够的)。如果是因为优化热点VM应用(方法内联),我想知道其他VM上的结果是否会有所不同。

修改

由于输出似乎是高度JVM特定的,我使用
得到结果4 Java(TM)SE运行时环境(版本1.6.0_41-b02)
Java HotSpot(TM)64位服务器VM(内置20.14-b01,混合模式)


解释为什么我认为这个问题与Understanding the Java stack不同:

我的问题不是为什么有一个cnt> 0(显然因为System.out.println()需要堆栈大小并在打印某些内容之前抛出另一个StackOverflowError),但为什么它具有特定值4,分别为0,3,8,55或其他系统上的其他内容

7 个答案:

答案 0 :(得分:41)

我认为其他人在解释为什么cnt>方面做得很好。 0,但是没有足够的细节关于为什么cnt = 4,以及为什么cnt在不同的设置中变化如此之大。我会尝试填补这个空白。

  • X是总堆栈大小
  • M是第一次进入main时使用的堆栈空间
  • 每次进入main
  • 时,R是堆栈空间增加
  • P是运行System.out.println
  • 所需的堆栈空间

当我们第一次进入主要时,留下的空间是X-M。每个递归调用占用R更多内存。因此,对于1个递归调用(比原始调用多1个),内存使用是M + R.假设在C成功递归调用之后抛出StackOverflowError,即M + C * R< = X和M + C *(R + 1)> X.在第一个StackOverflowError时,还有X-M-C * R内存。

为了能够运行System.out.prinln,我们需要在堆栈上留下P空间。如果发生X-M-C * R> = P,那么将打印0。如果P需要更多空间,那么我们从堆栈中删除帧,以cnt ++为代价获得R内存。

println最终能够运行时,X - M - (C - cnt)* R> = P.因此,如果P对于特定系统来说很大,则cnt将很大。

让我们看一些例子。

示例1:假设

  • X = 100
  • M = 1
  • R = 2
  • P = 1

然后C = floor((X-M)/ R)= 49,cnt = ceiling((P - (X - M - C * R))/ R)= 0.

示例2:假设

  • X = 100
  • M = 1
  • R = 5
  • P = 12

然后C = 19,cnt = 2。

示例3:假设

  • X = 101
  • M = 1
  • R = 5
  • P = 12

然后C = 20,cnt = 3.

示例4:假设

  • X = 101
  • M = 2
  • R = 5
  • P = 12

然后C = 19,cnt = 2。

因此,我们看到系统(M,R和P)和堆栈大小(X)都影响cnt。

作为旁注,catch需要多少空间才能启动。只要catch没有足够的空间,cnt就不会增加,所以没有外部效应。

修改

我收回了我对catch所说的内容。它确实发挥了作用。假设它需要T空间来启动。当剩余空间大于T时,cnt开始递增,当剩余空间大于T + P时,println运行。这为计算增加了额外的步骤,并进一步混淆了已经混乱的分析。

修改

我终于抽出时间进行一些实验来支持我的理论。不幸的是,该理论似乎与实验不符。实际发生的情况非常不同。

实验设置: Ubuntu 12.04服务器,默认为java和default-jdk。 Xss从70,000开始,以1字节为增量增加到460,000。

结果可在以下网址获得:https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM 我创建了另一个版本,其中删除了每个重复的数据点。换句话说,仅显示与先前不同的点。这样可以更容易地看到异常。 https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA

答案 1 :(得分:20)

这是错误的递归调用的受害者。当你想知道为什么 cnt 的值变化时,这是因为堆栈大小取决于平台。 Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k。您可以阅读更多here

您可以使用不同的堆栈大小运行,在堆栈溢出之前您会看到 cnt 的不同值 -

  

java -Xss1024k RandomNumberGenerator

您没有看到 cnt 的值被多次打印,即使该值大于1,因为您的print语句也会抛出错误,您可以调试以确保通过Eclipse或其他IDE。

如果您愿意,可以将代码更改为以下代码以调试每个语句的执行 -

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

<强>更新

随着这种关注越来越多,让我们有另一个例子来让事情变得更加清晰 -

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

我们创建了另一个名为 overflow 的方法来执行错误的递归,并从catch块中删除了 println 语句,因此它在尝试时不会再引发另一组错误打印。这按预期工作。您可以尝试在 cnt ++ 之后输入 System.out.println(cnt); 语句并进行编译。然后多次运行。根据您的平台,您可能会得到不同的 cnt 值。

这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想。

答案 2 :(得分:13)

行为取决于堆栈大小(可以使用Xss手动设置。堆栈大小是特定于体系结构的。来自JDK 7 source code

  

// Windows上的默认堆栈大小由可执行文件确定(java.exe
  //的默认值为320K / 1MB [32bit / 64bit])。根据Windows版本,更改
  // ThreadStackSize为非零可能会对内存使用产生重大影响   //请参阅os_windows.cpp中的注释。

因此,当抛出StackOverflowError时,错误会在catch块中捕获。这里println()是另一个堆栈调用,它再次抛出异常。这会重复出现。

重复多少次? - 这取决于JVM何时认为它不再是stackoverflow。这取决于每个函数调用(难以找到)和Xss的堆栈大小。如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的。因此行为不同。

使用java拨打-Xss 4M来电给我41。因此相关。

答案 3 :(得分:6)

我认为显示的数字是System.out.println来电引发Stackoverflow例外的时间。

这可能取决于println的实施情况以及在其中进行的堆叠调用次数。

举例说明:

main()调用在调用i时触发Stackoverflow异常。 main的i-1调用捕获异常并调用println,触发第二个Stackoverflowcnt增加到1。 主要捕获的i-2调用现在是异常并调用println。在println中,一种方法称为触发第三种异常。 cnt增加到2。   这一直持续到println可以进行所有需要的调用并最终显示cnt的值。

这取决于println的实际实施。

对于JDK7,它检测循环调用并提前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常,以便为补救逻辑提供一些空间,println实现不会调用++操作是在println调用之后完成的,因此通过了异常。

答案 4 :(得分:6)

  1. main自我递归,直到它以递归深度R溢出堆栈。
  2. 运行递归深度R-1的catch块。
  3. 递归深度R-1的catch块会评估cnt++
  4. 深度为R-1的catch块会调用println,将cnt的旧值放在堆栈上。 println将在内部调用其他方法并使用局部变量和事物。所有这些过程都需要堆栈空间。
  5. 由于堆栈已经放弃了限制,并且调用/执行println需要堆栈空间,因此在深度R-1而不是深度R处触发新的堆栈溢出。
  6. 步骤2-5再次发生,但递归深度为R-2
  7. 步骤2-5再次发生,但递归深度为R-3
  8. 步骤2-5再次发生,但递归深度为R-4
  9. 步骤2-4再次发生,但递归深度为R-5
  10. 现在有足够的堆栈空间可供println完成(请注意,这是一个实现细节,可能会有所不同)。
  11. cnt在深度R-1R-2R-3R-4以及最后R-5处进行了后递增。第五个后增量返回四,这就是打印的内容。
  12. main在深度R-5成功完成,整个堆栈展开,而不会运行更多的catch块并且程序完成。

答案 5 :(得分:1)

经过一段时间的挖掘后,我不能说我找到了答案,但我认为现在已经很接近了。

首先,我们需要知道何时会抛出StackOverflowError。实际上,java线程的堆栈存储了框架,其中包含调用方法和恢复所需的所有数据。根据{{​​3}},在调用方法时,

  

如果没有足够的可用内存来创建这样的激活帧,则抛出StackOverflowError。

其次,我们应该明确“没有足够的内存来创建这样的激活框架”。根据{{​​3}},

  

帧可能是堆分配的。

因此,当一个框架被创建时,应该有足够的堆空间来创建一个堆栈框架和足够的堆栈空间来存储新的引用,如果框架是堆分配的,那么它将指向新的堆栈框架。

现在让我们回到这个问题。从上面我们可以知道,当一个方法执行时,它可能只需要相同数量的堆栈空间。并且调用System.out.println(可能)需要5级方法调用,因此需要创建5个帧。然后当StackOverflowError被抛出时,它必须返回5次以获得足够的堆栈空间来存储5帧的引用。因此打印出4。为什么不5?因为您使用cnt++。将其更改为++cnt,然后您将获得5。

你会注意到当堆叠的大小达到高水平时,你有时会得到50。那是因为需要考虑可用堆空间的数量。当堆栈的大小太大时,堆栈空间可能会在堆栈之前耗尽。并且(可能)System.out.println的堆栈帧的实际大小约为main的51倍,因此它会返回51次并打印50次。

答案 6 :(得分:0)

这不完全是问题的答案,但我只想在我遇到的原始问题中添加一些内容以及我如何理解问题:

在原始问题中,异常会被捕获:

例如,对于jdk 1.7,它会在出现的第一个位置被捕获。

但是在早期版本的jdk中,看起来异常并没有在出现的第一个位置被捕获,因此4,50等。

现在,如果您删除try catch块,如下所示

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

然后你会看到cnt的所有值蚂蚁抛出异常(在jdk 1.7上)。

我使用netbeans来查看输出,因为cmd不会显示抛出的所有输出和异常。