Java 为什么导致StackOverflowerError的递归方法的调用计数在程序运行之间会有所不同?
用于演示的简单类:Java 为什么导致StackOverflowerError的递归方法的调用计数在程序运行之间会有所不同?,java,recursion,stack,stack-overflow,Java,Recursion,Stack,Stack Overflow,用于演示的简单类: public class Main { private static int counter = 0; public static void main(String[] args) { try { f(); } catch (StackOverflowError e) { System.out.println(counter); } } pri
public class Main {
private static int counter = 0;
public static void main(String[] args) {
try {
f();
} catch (StackOverflowError e) {
System.out.println(counter);
}
}
private static void f() {
counter++;
f();
}
}
我执行了上述程序5次,结果如下:
22025
22117
15234
21993
21430
为什么每次的结果都不一样
我尝试设置最大堆栈大小(例如-Xss256k
)。结果更加一致,但每次都不相等
Java版本:
java version "1.8.0_72"
Java(TM) SE Runtime Environment (build 1.8.0_72-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.72-b15, mixed mode)
编辑
当JIT被禁用时(-Djava.compiler=NONE
),我总是得到相同的数字(11907
)
这是有意义的,因为JIT优化可能会影响堆栈帧的大小,而且JIT所做的工作在执行过程中肯定会有所不同
尽管如此,我认为,如果通过参考一些关于主题的文档和/或JIT在这个特定示例中所做的导致帧大小变化的工作的具体示例来证实这一理论,那将是有益的。Java堆栈的确切功能未记录,但这完全取决于分配给该线程的内存
只需尝试使用stacksize的线程构造函数,看看它是否为常量。我还没有试过,所以请分享结果。首先,以下内容还没有研究过。我没有“深入研究”OpenJDK源代码来验证以下内容,也没有任何内部知识
$ java -Xint Main
11895
11895
11895
我试图通过在我的机器上运行测试来验证您的结果:
$ java -version
openjdk version "1.8.0_71"
OpenJDK Runtime Environment (build 1.8.0_71-b15)
OpenJDK 64-Bit Server VM (build 25.71-b15, mixed mode)
我得到的“计数”在~250范围内变化。(没有你看到的那么多)
首先是一些背景。典型Java实现中的线程堆栈是在线程启动之前分配的一个连续内存区域,它从不增长或移动。当JVM试图创建堆栈帧来进行方法调用,并且该帧超出内存区域的限制时,就会发生堆栈溢出。测试可以通过显式测试SP来完成,但我的理解是,它通常是通过使用内存页设置的巧妙技巧来实现的
分配堆栈区域时,JVM进行系统调用,告诉操作系统将堆栈区域末尾的“红色区域”页面标记为只读或不可访问。当线程发出溢出堆栈的调用时,它会访问“红色区域”中的内存,这会触发内存故障。操作系统通过一个“信号”告诉JVM,JVM的信号处理程序将其映射到线程堆栈上“抛出”的StackOverflowerError
因此,以下是对可变性的几种可能解释:
- 基于硬件的内存保护的粒度是页面边界。因此,如果线程堆栈是使用
分配的,那么该区域的开头将不会与页面对齐。因此,从堆栈帧的开始到“红色区域”(它>是<页面对齐的)的第一个字的距离将是可变的malloc
- “主”堆栈可能是特殊的,因为JVM引导时可能会使用该区域。这可能会导致在调用
之前在堆栈上留下一些“东西”。(这没有说服力……我也不相信。)main
f()
方法之后,stackframe的大小可能会有所不同。假设f()。如果JIT编译发生在不同的点,那么比率将不同。。。因此,当您达到极限时,计数将不同
尽管如此,我认为,如果通过参考一些关于主题的文档和/或JIT在这个特定示例中所做工作的具体示例来确认这一理论将是有益的,因为在这个特定示例中,JIT所做的工作会导致框架尺寸的变化
恐怕这样的可能性很小。。。除非你准备付钱给别人帮你做几天的研究
1) AFAIK,不存在此类(公共)参考文件。至少,我从来没能找到这类事情的确切来源。。。除了深入挖掘源代码
2) 查看JIT编译的代码并不能告诉您字节码解释器在代码被JIT编译之前是如何处理事情的。因此,您将无法查看帧大小是否已更改。观察到的差异是由后台JIT编译造成的
这就是过程的样子:
方法f()
在解释器中开始执行
在多次调用(大约250次)之后,该方法被安排进行编译
编译器线程与应用程序线程并行工作。同时,该方法继续在解释器中执行
一旦编译器线程完成编译,方法入口点就会被替换,因此对f()
的下一次调用将调用该方法的编译版本
应用程序线程和JIT编译器线程之间基本上存在一种竞争。在方法的编译版本准备就绪之前,解释器可能会执行不同数量的调用。最后是解释帧和编译帧的混合
$ java -XX:-BackgroundCompilation Main
23462
23462
23462
难怪编译后的框架布局和解释后的不同。编译帧通常较小;它们不需要将所有执行上下文存储在
$ java -Xcomp -XX:TieredStopAtLevel=1 Main
23720
23720
23720
$ java -Xcomp -XX:-TieredCompilation Main
59300
59300
59300
$ java -Xcomp -XX:-TieredCompilation -XX:CompileCommand=print,Main.f Main
0x00000000025ab460: mov %eax,-0x6000(%rsp) ; StackOverflow check
0x00000000025ab467: push %rbp ; frame link
0x00000000025ab468: sub $0x10,%rsp
0x00000000025ab46c: movabs $0xd7726ef0,%r10 ; r10 = Main.class
0x00000000025ab476: addl $0x2,0x68(%r10) ; Main.counter += 2
0x00000000025ab47b: callq 0x00000000023c6620 ; invokestatic f()
0x00000000025ab480: add $0x10,%rsp
0x00000000025ab484: pop %rbp ; pop frame
0x00000000025ab485: test %eax,-0x23bb48b(%rip) ; safepoint poll
0x00000000025ab48b: retq
$ java -XX:AbortVMOnException=java.lang.StackOverflowError Main
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 164 C2 Main.f()V (12 bytes) @ 0x00007f21251a5958 [0x00007f21251a5900+0x0000000000000058]
J 164 C2 Main.f()V (12 bytes) @ 0x00007f21251a5920 [0x00007f21251a5900+0x0000000000000020]
// ... repeated 19787 times ...
J 164 C2 Main.f()V (12 bytes) @ 0x00007f21251a5920 [0x00007f21251a5900+0x0000000000000020]
J 163 C1 Main.f()V (12 bytes) @ 0x00007f211dca50ec [0x00007f211dca5040+0x00000000000000ac]
J 163 C1 Main.f()V (12 bytes) @ 0x00007f211dca50ec [0x00007f211dca5040+0x00000000000000ac]
// ... repeated 1866 times ...
J 163 C1 Main.f()V (12 bytes) @ 0x00007f211dca50ec [0x00007f211dca5040+0x00000000000000ac]
j Main.f()V+8
j Main.f()V+8
// ... repeated 1839 times ...
j Main.f()V+8
j Main.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub