flr*_*rnb 111 java stack-overflow jvm
我想知道当你试图捕获StackOverflowError时会发生什么,并提出以下方法:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在我的问题:
为什么这个方法打印'4'?
我想也许是因为System.out.println()在调用堆栈上需要3个段,但我不知道3号来自哪里.当你查看源代码(和字节码)时System.out.println(),它通常会导致比3更多的方法调用(因此调用堆栈上的3个段是不够的).如果是因为优化热点VM应用(方法内联),我想知道其他VM上的结果是否会有所不同.
编辑:
由于输出似乎是高度JVM特定的,我使用
Java(TM)SE运行时环境(构建1.6.0_41-b02)
Java HotSpot(TM)64位服务器VM(构建20.14-b01,混合模式)得到结果4
解释为什么我认为这个问题与理解Java堆栈不同:
我的问题不是为什么有一个cnt> 0(显然是因为System.out.println()需要堆栈大小并StackOverflowError在某些东西被打印之前抛出另一个),但为什么它具有特定值4,分别为0,3,8,55或其他的其他东西系统.
Joh*_*eng 41
我认为其他人已经很好地解释了为什么cnt> 0,但是关于为什么cnt = 4没有足够的细节,以及为什么cnt在不同的设置中变化如此之大.我会在这里填补这个空白.
让
System.out.println当我们第一次进入main时,留下的空间是XM.每个递归调用占用R更多内存.因此对于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需要更多空间,那么我们从堆栈中删除帧,以cnt ++为代价获得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开始递增,println当剩余空间大于T + P 时,cnt 运行.这为计算增加了额外的步骤,并进一步混淆了已经泥泞的分析.
编辑
我终于抽出时间进行一些实验来支持我的理论.不幸的是,该理论似乎与实验不符.实际发生的情况非常不同.
实验设置:Ubuntu 12.04服务器,默认为java和default-jdk.Xss从70,000开始,以1字节为增量,达到460,000.
结果可从以下网址获得:https://www.google.com/fusiontables/DataSource?docid = 1xkJhd4s8biLghe6gZccfUs3vT5MpS_OnscjWDbM 我创建了另一个版本,其中删除了每个重复的数据点.换句话说,仅显示与先前不同的点.这样可以更容易地看到异常.https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
Saj*_*tta 20
这是糟糕的递归调用的受害者.当您想知道为什么cnt的值变化时,这是因为堆栈大小取决于平台.Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k.你可以在这里阅读更多.
你可以使用不同的堆栈大小运行,在堆栈溢出之前你会看到不同的cnt值 -
java -Xss1024k RandomNumberGenerator
您没有看到cnt的值被多次打印,即使该值大于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) {
}
}
}
Run Code Online (Sandbox Code Playgroud)
更新:
随着这一点得到更多的关注,让我们有另一个例子来让事情变得更加清晰 -
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);
}
Run Code Online (Sandbox Code Playgroud)
我们创建了另一个名为overflow的方法来执行错误的递归,并从catch块中删除了println语句,因此在尝试打印时它不会开始抛出另一组错误.这按预期工作.你可以试试把System.out.println(cnt); cnt ++之后的语句和编译.然后多次运行.根据您的平台,您可能会获得不同的cnt值.
这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想.
Jat*_*tin 13
行为取决于堆栈大小(可以使用手动设置Xss.堆栈大小是特定于体系结构的.来自JDK 7 源代码:
// Windows上的默认堆栈大小由可执行文件决定(java.exe
//默认值为320K/1MB [32bit/64bit]).根据Windows版本,将
// ThreadStackSize 更改为非零可能会对内存使用产生重大影响.
//请参阅os_windows.cpp中的注释.
所以当StackOverflowError抛出时,错误会在catch块中被捕获.这println()是另一个再次抛出异常的堆栈调用.这会重复出现.
重复多少次? - 这取决于JVM何时认为它不再是stackoverflow.这取决于每个函数调用的堆栈大小(很难找到)和Xss.如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的.因此不同的行为.
拨打java电话-Xss 4M给了我41.因此相关性.
我认为显示的数字是System.out.println调用抛出Stackoverflow异常的时间.
它可能取决于它的实现println以及在其中进行的堆叠调用的数量.
作为说明:
该main()呼叫Stackoverflow在呼叫i时触发异常.主要的i-1调用捕获异常并调用println触发一秒的异常Stackoverflow. cnt获得增量为1.主要捕获的i-2调用现在异常并调用println.在println一个方法中称为触发第三个异常. cnt得到增量为2.这继续直到println可以进行所有需要的调用并最终显示其值cnt.
这取决于实际的实施println.
对于JDK7,它要么检测循环调用并且先提前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常,为补救逻辑提供一些空间,要么println实现不进行调用,要么执行++操作println因此,呼叫是通过异常.
main自我递归直到它在递归深度溢出堆栈R.R-1运行.R-1进行评估cnt++.R-1调用的catch块println,将cnt旧值放在堆栈上.println将在内部调用其他方法并使用局部变量和事物.所有这些过程都需要堆栈空间.println需要堆栈空间,所以在深度R-1而不是深度处触发新的堆栈溢出R.R-2.R-3.R-4.R-5.println来完成(注意这是一个实现细节,它可能会有所不同).cnt在递增后的深度R-1,R-2,R-3,R-4,终于在R-5.第五个后增量返回四,这是打印的.main在深度成功完成R-5,整堆解开,而不运行多个catch块和程序完成.