为什么这个方法打印4?

flr*_*rnb 111 java stack-overflow jvm

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

class RandomNumberGenerator {

    static int cnt = 0;

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

现在我的问题:

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

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

编辑:

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


解释为什么我认为这个问题与理解Java堆栈不同:

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

Joh*_*eng 41

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

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

当我们第一次进入main时,留下的空间是XM.每个递归调用占用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((XM)/ 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开始递增,println当剩余空间大于T + P 时,cnt 运行.这为计算增加了额外的步骤,并进一步混淆了已经泥泞的分析.

编辑

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

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

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


Saj*_*tta 20

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

你可以使用不同的堆栈大小运行,在堆栈溢出之前你会看到不同的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) {   

        }        
    }        
}
Run Code Online (Sandbox Code Playgroud)

更新:

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

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);

}
Run Code Online (Sandbox Code Playgroud)

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

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


Jat*_*tin 13

行为取决于堆栈大小(可以使用手动设置Xss.堆栈大小是特定于体系结构的.来自JDK 7 源代码:

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

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

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

拨打java电话-Xss 4M给了我41.因此相关性.

  • 我不明白为什么堆栈大小会影响结果,因为当我们尝试打印cnt的值时已经超出了它.因此,唯一的区别可能来自"每个函数调用的堆栈大小".我不明白为什么这应该在运行相同JVM版本的2台机器之间有所不同. (4认同)

Kaz*_*aag 6

我认为显示的数字是System.out.println调用抛出Stackoverflow异常的时间.

它可能取决于它的实现println以及在其中进行的堆叠调用的数量.

作为说明:

main()呼叫Stackoverflow在呼叫i时触发异常.主要的i-1调用捕获异常并调用println触发一秒的异常Stackoverflow. cnt获得增量为1.主要捕获的i-2调用现在异常并调用println.在println一个方法中称为触发第三个异常. cnt得到增量为2.这继续直到println可以进行所有需要的调用并最终显示其值cnt.

这取决于实际的实施println.

对于JDK7,它要么检测循环调用并且先提前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常,为补救逻辑提供一些空间,要么println实现不进行调用,要么执行++操作println因此,呼叫是通过异常.


Cra*_*ney 6

  1. main自我递归直到它在递归深度溢出堆栈R.
  2. 递归深度处的catch块R-1运行.
  3. 递归深度处的catch块R-1进行评估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-1,R-2,R-3,R-4,终于在R-5.第五个后增量返回四,这是打印的.
  12. 随着main在深度成功完成R-5,整堆解开,而不运行多个catch块和程序完成.