我想知道当你试图捕获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上的结果是否会有所不同 .
Edit :
由于输出似乎是高度JVM特定的,我得到结果4使用
Java(TM)SE运行时环境(版本1.6.0_41-b02)
Java HotSpot(TM)64位服务器VM(内置20.14-b01,混合模式)
Explanation why I think this question is different from Understanding the Java stack:
我的问题不是为什么有一个cnt> 0(显然是因为 System.out.println()
需要堆栈大小并在某些东西被打印之前抛出另一个 StackOverflowError
),但为什么它具有特定值4,分别为0,3,8,55或其他在其他系统上 .
7 回答
我认为其他人已经很好地解释了为什么cnt> 0,但是关于为什么cnt = 4没有足够的细节,以及为什么cnt在不同的设置中变化如此之大 . 我会在这里填补这个空白 .
让
X是总堆栈大小
M是我们第一次进入main时使用的堆栈空间
R每次进入main时堆栈空间增加
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存储器 .当
println
最终能够运行时,X - M - (C - cnt)* R> = P.因此,如果P对于特定系统而言很大,则cnt将很大 .让我们用一些例子来看看这个 .
Example 1: 假设
X = 100
M = 1
R = 2
P = 1
然后C = floor((X-M)/ R)= 49,并且cnt = ceiling((P - (X-M-C * R))/ R)= 0 .
Example 2: 假设
X = 100
M = 1
R = 5
P = 12
然后C = 19,cnt = 2 .
Example 3: 假设
X = 101
M = 1
R = 5
P = 12
然后C = 20,cnt = 3 .
Example 4: 假设
X = 101
M = 2
R = 5
P = 12
然后C = 19,cnt = 2 .
因此,我们看到系统(M,R和P)和堆栈大小(X)都影响cnt .
作为旁注,
catch
需要多少空间才能启动 . 只要catch
没有足够的空间,cnt就不会增加,所以没有外部效果 .EDIT
我收回了我所说的
catch
. 它确实发挥了作用 . 假设它需要T空间来启动 . 当剩余空间大于T时,cnt开始递增,当剩余空间大于T P时,ctt开始运行 . 这为计算增加了额外的步骤,并进一步混淆了已经混浊的分析 .EDIT
我终于抽出时间进行一些实验来支持我的理论 . 不幸的是,该理论似乎与实验不符 . 实际发生的情况非常不同 .
实验设置: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
这是糟糕的递归调用的受害者 . 当您想知道为什么cnt的值变化时,这是因为堆栈大小取决于平台 . Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k . 你可以阅读更多here .
你可以使用不同的堆栈大小运行,在堆栈溢出之前你会看到不同的cnt值 -
您没有看到cnt的值被多次打印,即使该值大于1,因为您的print语句也会抛出错误,您可以通过Eclipse或其他IDE调试该错误 .
如果您愿意,可以将代码更改为以下代码以调试每个语句的执行情况 -
UPDATE:
随着这一点得到更多的关注,让我们有另一个例子来让事情变得更加清晰 -
我们创建了另一个名为overflow的方法来执行错误的递归,并从catch块中删除了println语句,因此在尝试打印时它不会开始抛出另一组错误 . 这按预期工作 . 你可以试试把System.out.println(cnt); cnt上面的语句和编译 . 然后多次运行 . 根据您的平台,您可能会获得不同的cnt值 .
这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想 .
行为取决于堆栈大小(可以使用
Xss
手动设置 . 堆栈大小是特定于体系结构的 . 来自JDK 7 source code:因此,当抛出
StackOverflowError
时,错误会在catch块中捕获 . 这里println()
是另一个再次抛出异常的堆栈调用 . 这会重复出现 .重复多少次? - 这取决于JVM何时认为它不再是stackoverflow . 这取决于每个函数调用的堆栈大小(很难找到)和
Xss
. 如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的 . 因此不同的行为 .用
-Xss 4M
调用java
调用给了我41
. 因此相关性 .我认为显示的数字是
System.out.println
调用抛出Stackoverflow
异常的时间 .它可能取决于
println
的实现以及它在其中进行的堆叠调用次数 .作为说明:
main()
调用在调用i时触发Stackoverflow
异常 . main的i-1调用捕获异常并调用println
,触发第二个Stackoverflow
.cnt
得到增量为1.主要捕获的i-2调用现在是异常并调用println
. 在println
中,一个方法被称为触发第三个异常 .cnt
得到增量到2.这继续直到println
可以进行所有需要的调用,最后显示cnt
的值 .这取决于
println
的实际实现 .对于JDK7,它要么检测循环调用并在之前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常以给予补救逻辑一些空间
println
实现不会调用操作或者操作完成后println
因此通过异常调用 .main
自我递归直到它在递归深度R
溢出堆栈 .运行递归深度
R-1
处的catch块 .递归深度
R-1
处的catch块计算cnt++
.深度为
R-1
的catch块调用println
,将cnt
的旧值放在堆栈上 .println
将在内部调用其他方法并使用局部变量和事物 . 所有这些过程都需要堆栈空间 .因为堆栈已经放弃了限制,并且调用/执行
println
需要堆栈空间,所以在深度R-1
而不是深度R
处触发新的堆栈溢出 .步骤2-5再次发生,但在递归深度
R-2
.步骤2-5再次发生,但在递归深度
R-3
.步骤2-5再次发生,但在递归深度
R-4
.步骤2-4再次发生,但在递归深度
R-5
.碰巧现在有足够的堆栈空间来完成
println
(注意这是一个实现细节,它可能会有所不同) .cnt
在深度R-1
,R-2
,R-3
,R-4
后递增,最后在R-5
. 第五个后增量返回四,这是打印的 .随着
main
在深度R-5
成功完成,整个堆栈展开而不会运行更多的catch块并且程序完成 .经过一段时间的挖掘后,我不能说我找到了答案,但我认为现在已经很接近了 .
首先,我们需要知道何时抛出
StackOverflowError
. 实际上,java线程的堆栈存储了框架,其中包含调用方法和恢复所需的所有数据 . 根据Java Language Specifications for JAVA 6,在调用方法时,其次,我们应该明确“没有足够的内存来创建这样的激活框架” . 根据Java Virtual Machine Specifications for JAVA 6,
因此,当创建一个框架时,应该有足够的堆空间来创建一个堆栈框架和足够的堆栈空间来存储新的引用,如果框架是堆分配的,则该引用指向新的堆栈框架 .
现在让我们回到这个问题 . 从上面我们可以知道,当一个方法执行时,它可能只需要相同数量的堆栈空间 . 并且调用
System.out.println
(可能)需要5级方法调用,因此需要创建5个帧 . 然后当StackOverflowError
被抛出时,它必须返回5次以获得足够的堆栈空间来存储5帧的引用 . 因此打印出4 . 为什么不5?因为你使用cnt++
. 将其更改为++cnt
,然后您将获得5 .而且你会注意到当堆栈的大小达到很高的水平时,你有时会得到50 . 那是因为需要考虑可用堆空间的数量 . 当堆栈的大小太大时,堆栈空间可能会在堆栈之前耗尽 . 并且(可能)
System.out.println
的堆栈帧的实际大小约为main
的51倍,因此它返回51次并打印50 .这不是问题的答案,但我只想在我遇到的原始问题中添加一些内容以及我如何理解问题:
在最初的问题中,异常会被捕获到可能的位置:
例如,对于jdk 1.7,它会在出现的第一个位置被捕获 .
但是在早期版本的jdk中,看起来异常并没有在出现的第一个位置被捕获,因此4,50等 .
现在,如果您删除try catch块,如下所示
然后你会看到
cnt
的所有值 Ant 抛出异常(在jdk 1.7上) .我使用netbeans来查看输出,因为cmd不会显示所有输出和异常抛出 .