为什么此方法打印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位服务器虚拟机(构建20.14-b01,混合模式)
解释为什么我认为这个问题与理解Java堆栈不同:
我的问题不是关于为什么有一个cnt> 0(显然是因为System.out.println()
需要堆栈大小,并在打印之前抛出另一个StackOverflowError
),但为什么它具有特殊值4,分别为0,3, 8,55或其他系统上的其他内容。
我认为其他人在解释为什么cnt> 0方面做得很好,但是为什么cnt = 4没有足够的细节,以及为什么cnt在不同环境中变化很大。 我会尽力填补这个空白。
让
System.out.println
所需的堆栈空间 当我们第一次进入主体时,剩下的空间就是XM。 每次递归调用都会占用更多的内存。 因此,对于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需要更多空间,那么我们从堆栈中移除帧,以c ++ ++为代价获得R内存。
当println
最终能够运行时,X - M - (C - cnt)* R> = P。因此,如果P对于特定系统很大,那么cnt将会很大。
我们来看一些例子。
例1:假设
那么C = floor((XM)/ R)= 49,并且cnt = ceiling((P - (X-M-C * R))/ R)= 0。
例2:假设
然后C = 19,cnt = 2。
例3:假设
然后C = 20,cnt = 3。
例4:假设
然后C = 19,cnt = 2。
因此,我们看到系统(M,R和P)和堆栈大小(X)都会影响cnt。
作为一个侧面说明, catch
空间需要多少空间并不重要。 只要没有足够的catch
空间,cnt就不会增加,所以没有外部影响。
编辑
我收回我说的关于catch
。 它确实发挥作用。 假设它需要T个空间来启动。 当剩余空间大于T时,cnt开始递增,并且当剩余空间大于T + P时, println
运行。这为计算增加了一个额外的步骤,并进一步混淆了已经泥泞的分析。
编辑
我终于找到时间运行一些实验来支持我的理论。 不幸的是,这个理论似乎并不符合实验。 实际发生的事情是非常不同的。
实验设置:使用默认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
这是糟糕的递归调用的受害者。 正如你想知道为什么cnt的值会变化,这是因为栈的大小取决于平台。 Windows上的Java SE 6在32位虚拟机中的默认堆栈大小为320k,在64位虚拟机中的默认堆栈大小为1024k。 你可以在这里阅读更多。
您可以使用不同的堆栈大小运行,您会在堆栈溢出之前看到不同的cnt值 -
java -Xss1024k RandomNumberGenerator
即使该值大于1,也不会看到多次打印cnt的值,因为您的打印语句也会抛出错误,您可以通过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值。
这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想。
行为取决于堆栈大小(可以使用Xss
手动设置堆栈大小,堆栈大小是特定于架构的。从JDK 7源代码:
// Windows上的默认堆栈大小由可执行文件(java.exe
//默认值为320K / 1MB [32位/ 64位])。 取决于Windows版本,更改
// ThreadStackSize为非零可能会对内存使用产生重大影响。
//请参阅os_windows.cpp中的注释。
所以当引发StackOverflowError
时,错误会被catch块捕获。 这里println()
是另一个堆栈调用,它再次抛出异常。 这得到重复。
它重复多少次? - 这取决于JVM何时认为它不再是stackoverflow。 这取决于每个函数调用(很难找到)和Xss
的堆栈大小。 如上所述,每个函数调用的默认总大小和大小(取决于内存页大小等)是平台特定的。 因此不同的行为。
用-Xss 4M
调用java
调用给我41
。 因此是相关性。