问题
我想知道当你试图捕获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上的结果是否会有所不同。
编辑:
由于输出似乎是高度JVM特定的,我得到结果4使用
Java(TM)SE运行时环境(版本1.6.0_41-b02)
Java HotSpot(TM)64位服务器VM(内置20.14-b01,混合模式)
解释为什么我认为这个问题与Understanding the Java stack不同:
我的问题不是为什么有一个cnt> 0(显然是因为System.out.println()
需要堆栈大小并在某些内容被打印之前抛出另一个StackOverflowError
),但是为什么它具有特定值4,分别为0,3,8,55或其他系统上的其他东西。
#1 热门回答(41 赞)
我认为其他人已经很好地解释了为什么cnt> 0,但是关于为什么cnt = 4没有足够的细节,以及为什么cnt在不同的设置中变化如此之大。我会在这里填补这个空白。
让
- X是总堆栈大小
- M是我们第一次进入main时使用的堆栈空间
- 每次进入main时,R都是堆栈空间增加
- 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存储器。
当最终能够运行时,X - M - (C - cnt)* R> = P.因此,如果P对于特定系统来说很大,则cnt将很大。
让我们通过一些例子来看看这个。
**示例1:**Suppose
- X = 100
- M = 1
- R = 2
- P = 1
然后C = floor((X-M)/ 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开始递增,当剩余空间大于T P时,cnt开始递增。这为计算增加了额外的步骤,并进一步混淆了已经混浊的分析。
编辑
我终于抽出时间进行一些实验来支持我的理论。不幸的是,该理论似乎与实验不符。实际发生的情况非常不同。
实验设置: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
#2 热门回答(20 赞)
这是错误的递归调用的受害者。正如你想知道为什么值的变量,这是因为堆栈大小取决于平台。 Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k。你可以阅读更多here。
你可以使用不同的堆栈大小运行,并且在堆栈溢出之前你将看到不同的值 -
java -Xss1024k RandomNumberGenerator
即使值大于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);
}
我们创建了另一个方法namedoverflow来执行错误的递归并从catch块中删除了printlnstatement,因此在尝试打印时它不会开始抛出另一组错误。这按预期工作。你可以尝试putSystem.out.println(cnt);语句上面的aftercnt并编译。然后运行多次。根据你的平台,你可能会获得不同的值。
这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想。
#3 热门回答(13 赞)
行为取决于堆栈大小(可以使用Xss
手动设置。堆栈大小是特定于体系结构的。来自JDK 7source code:
// Windows上的默认堆栈大小由可执行文件决定(java.exe //默认值为320K / 1MB [32bit / 64bit])。根据Windows版本,将// ThreadStackSize更改为非零可能会对内存使用产生重大影响。 //请参阅os_windows.cpp中的注释。
所以当抛出StackOverflowError
时,错误会在catch块中被捕获。 Hereprintln()
是另一个再次抛出异常的堆栈调用。这会重复出现。
重复多少次? - 这取决于JVM何时认为它不再是stackoverflow。这取决于每个函数调用(难以找到)和Xss
的堆栈大小。如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的。因此不同的行为。
拨打电话java
电话号码为-Xss 4M
,电话号码为me41
。因此相关。