为什么这个方法打印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个段将不够用)。 如果是因为热点虚拟机应用(方法内联)的优化,我想知道在另一个虚拟机上的结果会不同。

编辑

由于输出看起来是JVM特有的,我得到了使用的结果
Java(TM)SE运行时环境(build 1.6.0_41-b02)
Java HotSpot(TM)64位服务器虚拟机(构build20.14-b01,混合模式)

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

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

我认为其他人在解释为什么cnt> 0时做得很好,但是为什么cnt = 4没有足够的细节,以及为什么cnt在不同的环境中变化很大。 我会试图填补这个空白。

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

当我们进入主体时,剩下的空间是XM。 每个recursion调用占用R更多的内存。 因此,对于1次recursion调用(比原来多1次),内存使用是M + R.假设在C成功recursion调用之后抛出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需要更多的空间,那么我们从栈中移除帧,以c ++ ++为代价获得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多less空间需要启动并不重要。 只要没有足够的catch空间,cnt就不会增加,所以没有外部影响。

编辑

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

编辑

我终于find时间来做一些实验来支持我的理论。 不幸的是,这个理论似乎并不符合实验。 实际发生的事情是非常不同的。

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

结果可在以下urlfind: https : //www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM我创build了另一个版本,每个重复的数据点都被删除。 换句话说,只显示与以前不同的点。 这使得更容易看到exception。 https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA

这是糟糕的recursion调用的受害者。 正如你想知道为什么cnt的值有所不同,这是因为栈的大小取决于平台。 Windows上的Java SE 6在32位虚拟机中的默认堆栈大小为320k,在64位虚拟机中的默认堆栈大小为1024k。 你可以在这里阅读更多。

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

java -Xss1024k RandomNumberGenerator

即使值大于1,也不会看到多次打印cnt的值,因为您的打印语句也会抛出错误,您可以通过Eclipse或其他IDEdebugging错误。

如果您愿意,可以将代码更改为以下语句来debugging每个语句的执行:

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

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

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

该行为取决于堆栈的大小(可以使用Xss手动设置堆栈的大小是特定于架构的,从JDK 7 源代码 :

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

所以当抛出StackOverflowError时,错误被catch块捕获。 这里println()是另一个堆栈调用,它再次抛出exception。 这得到重复。

多less次重复? – 这取决于什么时候JVM认为它不再是stackoverflow。 这取决于每个函数调用(很难find)和Xss的堆栈大小。 如上所述,每个函数调用的默认总大小和大小(取决于内存页大小等)是平台特定的。 因此不同的行为。

-Xss 4M调用java调用给我41 。 因此相关。

我认为显示的数字是System.out.println调用抛出Stackoverflowexception的时间。

这可能取决于println的实现以及堆栈调用的数量。

作为一个例子:

main()调用在调用i时触发Stackoverflowexception。 主要的i-1调用捕获exception,并调用println触发第二个Stackoverflowcnt获得增量1.主要的i-2调用现在捕获exception并调用println 。 在println一个方法被称为触发第三个exception。 cnt得到2的增量。直到println可以完成所有需要的调用,最后显示cnt的值。

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

对于JDK7,要么检测到循环调用,而是要提前抛出exception,或者保留一些堆栈资源,并在达到限制之前抛出exception,为补救逻辑留出空间,否则println实现不会调用++操作完成在println调用之后由exception通过。

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

经过一段时间的挖掘,我不能说我find了答案,但我认为现在已经非常接近了。

首先,我们需要知道什么时候会抛出StackOverflowError 。 事实上,Java线程的堆栈存储了包含调用方法和恢复所需的全部数据的框架。 根据JAVA 6的Java语言规范 ,在调用方法时,

如果没有足够的内存可用来创build这样的激活帧,则会引发StackOverflowError。

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

帧可能被分配堆。

所以,当一个框架被创build时,如果框架被分配,应该有足够的堆空间来创build一个堆栈框架和足够的堆栈空间来存储指向新堆栈框架的新引用。

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

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

这不完全是问题的答案,但我只是想添加一些原来的问题,我遇到了,我怎么理解这个问题:

在最初的问题中,例外被捕获在可能的地方:

例如,用jdk 1.7它被捕获在第一的地方。

但在jdk的早期版本中,看起来exception并没有被第一个出现的地方,因此4,5等。

现在,如果你删除try catch块如下

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

然后你会看到所有cnt ant的值抛出exception(在JDK1.7上)。

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