为什么此方法打印4?


111

我想知道当您尝试捕获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个段是不够的)。如果是由于优化而应用了Hotspot VM(方法内联),我想知道在另一个VM上结果是否会有所不同。

编辑

由于输出似乎是高度特定于JVM的,因此我使用
Java(TM)SE Runtime Environment(内部版本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或其他值系统。


4
在我的本地环境中,我的预期值为“ 0”。
Reddy

2
这可能涉及许多架构问题。所以最好用jdk版本发布您的输出。对我来说,jdk 1.7上的输出为0
Lokesh

3
我已经得到了5638与Java 1.7.0_10

8
当您执行涉及基础架构的技巧时,@ Elist不会是相同的输出;)
m0skit0

3
@flrnb这只是我用来排列括号的样式。它使我更容易知道条件和功能的开始和结束位置。您可以根据需要进行更改,但是我认为这种方式更具可读性。
syb0rg

Answers:


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 运行。这为计算增加了一个额外的步骤,并进一步使本来已经很混乱的分析变得更加混乱。

编辑

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

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

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


谢谢您的总结,我想这全都归结为一个问题:M,R和P有什么影响(因为X可以通过VM选项-Xss设置)?
flrnb

@flrnb M,R和P是系统特定的。您不能轻易更改它们。我希望它们在某些版本之间也会有所不同。
曾庆华

然后,为什么通过更改Xss(又称X)得到不同的结果?假设M,R和P保持不变,则将X从100更改为10000,应该不会根据您的公式影响cnt,还是我弄错了?
flrnb

由于这些变量的离散性,仅@flrnb X确实会更改cnt。示例2和3仅在X上不同,但cnt不同。
曾俊

1
@JohnTseng我也认为你的回答是最理解并完成了-无论如何,我会很感兴趣的是堆栈实际上看起来像瞬间StackOverflowError被抛出,这是否是如何影响输出。如果它仅包含对堆上堆栈帧的引用(如Jay所建议的),那么对于给定的系统,输出应该是可预测的。
flrnb

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

        }        
    }        
}

更新:

由于这引起了越来越多的关注,让我们再举一个例子来使事情变得更清楚:

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语句,因此它在尝试打印时不会开始引发另一组错误。这按预期工作。您可以尝试放入System.out.println(cnt); 以上cnt ++之后的语句并进行编译。然后运行多次。根据您的平台,您可能会获得不同的cnt值。

这就是为什么我们通常不会捕获错误的原因,因为代码中的奥秘不是幻想。


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。因此,相关性。


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

只能从JVM源获得确切的行为。但是原因可能是这样。请记住,偶数catch是一个块,并且会占用堆栈中的内存。每个方法调用占用多少内存是未知的。清除堆栈后,您将再添加一个catch,以此类推。这可能是行为。这只是猜测。
加丁

堆栈大小在两台不同的计算机中可能有所不同。堆栈大小取决于许多基于os的因素,即内存页面大小等
Jatin

6

我认为显示的数字是System.out.println调用引发Stackoverflow异常的次数。

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

举例说明:

main()呼叫Stackoverflow在呼叫i处触发异常。main的i-1调用捕获异常并println触发第二个Stackoverflowcnt得到增量为1。现在,main catch的i-2调用异常并调用println。在println一个方法中称为触发第三异常。 cnt将增量增加到2。继续操作直到println可以进行所有需要的调用并最终显示的值。cnt

然后,这取决于的实际实现println

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


这就是我的意思,“我认为可能是因为System.out.println在调用堆栈上需要3个段”-但我感到困惑的是,为什么它恰好是这个数字,而现在我更加困惑的是,为什么这个数字之间的差异如此之大(虚拟)机器
flrnb

我对此表示部分同意,但不同意的是语句“取决于println的实际实现。”它与每个jvm中的堆栈大小有关,而不是与实现有关。
加丁

6
  1. main自身递归直到递归深度溢出堆栈R
  2. R-1运行递归深度的catch块。
  3. 对递归深度的catch块R-1进行评估cnt++
  4. 深度R-1调用的catch块printlncnt的旧值放在堆栈上。println将在内部调用其他方法并使用局部变量和事物。所有这些过程都需要堆栈空间。
  5. 由于堆栈已经达到了极限,并且调用/执行println需要堆栈空间,因此将在深度R-1而不是depth 处触发新的堆栈溢出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块整个堆栈退绕正在运行和程序完成。

1

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

首先,我们需要知道何时StackOverflowError会抛出a。实际上,Java线程的堆栈存储框架,其中包含调用方法和恢复所需的所有数据。根据JAVA 6的Java语言规范,在调用方法时,

如果没有足够的内存来创建这样的激活框架,则会引发StackOverflowError。

其次,我们应该弄清楚什么是“ 没有足够的内存来创建这样的激活框架 ”。根据JAVA 6的Java虚拟机规范

帧可能是堆分配的。

因此,在创建框架时,应该有足够的堆空间来创建堆栈框架,并且要有足够的堆栈空间来存储新引用,如果该框架是堆分配的,则指向新堆栈框架。

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

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


我的第一个想法也是在计算方法调用的级别(您是对的,我没有注意我发布了int cnt的事实),但是如果解决方案是如此简单,为什么结果在平台之间会有如此大的差异和VM实施?
flrnb

@flrnb这是因为不同的平台可能会影响堆栈框架的大小,并且不同版本的jre将影响的实现System.out.print或方法执行策略。如上所述,VM实施还影响堆栈帧的实际存储位置。
杰伊,

0

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

在原始问题中,在可能的地方捕获了异常:

例如,对于jdk 1.7,它是在第一发生的地方被捕获的。

但是在早期版本的jdk中,似乎没有在第一发生的位置捕获异常,因此出现了4,50等。

现在,如果您按以下方式删除try catch块

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

然后,您将看到的所有值 cnt ant引发的异常的(在jdk 1.7上)。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.