为什么这种方法打印4?

问题

我想知道当你试图捕获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或其他系统上的其他东西。


#1 热门回答(41 赞)

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

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

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

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

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

让我们通过一些例子来看看这个。

**示例1:**Suppose

  • 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时,cnt开始递增。这为计算增加了额外的步骤,并进一步混淆了已经混浊的分析。
编辑
我终于抽出时间进行一些实验来支持我的理论。不幸的是,该理论似乎与实验不符。实际发生的情况非常不同。

实验设置: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


#2 热门回答(20 赞)

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

你可以使用不同的堆栈大小运行,并且在堆栈溢出之前你将看到不同的值 -

java -Xss1024k RandomNumberGenerator

即使值大于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);

}

我们创建了另一个方法namedoverflow来执行错误的递归并从catch块中删除了printlnstatement,因此在尝试打印时它不会开始抛出另一组错误。这按预期工作。你可以尝试putSystem.out.println(cnt);语句上面的aftercnt并编译。然后运行多次。根据你的平台,你可能会获得不同的值。

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


#3 热门回答(13 赞)

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

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

所以当抛出StackOverflowError时,错误会在catch块中被捕获。 Hereprintln()是另一个再次抛出异常的堆栈调用。这会重复出现。

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

拨打电话java电话号码为-Xss 4M,电话号码为me41。因此相关。