为什么这个方法打印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
调用抛出Stackoverflow
exception的时间。
这可能取决于println
的实现以及堆栈调用的数量。
作为一个例子:
main()
调用在调用i时触发Stackoverflow
exception。 主要的i-1调用捕获exception,并调用println
触发第二个Stackoverflow
。 cnt
获得增量1.主要的i-2调用现在捕获exception并调用println
。 在println
一个方法被称为触发第三个exception。 cnt
得到2的增量。直到println
可以完成所有需要的调用,最后显示cnt
的值。
然后,这取决于println
的实际实现。
对于JDK7,要么检测到循环调用,而是要提前抛出exception,或者保留一些堆栈资源,并在达到限制之前抛出exception,为补救逻辑留出空间,否则println
实现不会调用++操作完成在println
调用之后由exception通过。
-
main
recursion本身,直到它在recursion深度R
溢出堆栈。 - 在recursion深度
R-1
的catch块被运行。 - recursion深度
R-1
的catch块评估cnt++
。 - 深度为
R-1
的catch块调用println
,将cnt
的旧值放在堆栈上。println
将在内部调用其他方法并使用局部variables和事物。 所有这些进程都需要堆栈空间。 - 因为堆栈已经放弃了限制,并且调用/执行
println
需要堆栈空间,所以在深度R-1
而不是深度R
触发新的堆栈溢出。 - 步骤2-5再次发生,但在recursion深度
R-2
。 - 步骤2-5再次发生,但在recursion深度
R-3
。 - 步骤2-5再次发生,但在recursion深度
R-4
。 - 步骤2-4再次发生,但在recursion深度
R-5
。 - 恰好现在有足够的空间来完成
println
(请注意,这是一个实现细节,可能会有所不同)。 -
cnt
在深度R-1
,R-2
,R-3
,R-4
以及最后在R-5
处后递增。 第五次后增加返回四,这是印刷。 - 随着
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抛出。