gpt4 book ai didi

java - 为什么导致 StackOverflowError 的递归方法的调用次数在程序运行之间会有所不同?

转载 作者:IT老高 更新时间:2023-10-28 21:03:36 25 4
gpt4 key购买 nike

一个用于演示的简单类:

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 在此特定示例中所做的工作的具体示例来证实这一理论,这将是有益的,这会导致帧大小发生变化。

最佳答案

观察到的差异是由后台 JIT 编译引起的。

流程如下:

  1. 方法 f() 开始在解释器中执行。
  2. 在多次调用(大约 250 次)之后,该方法被安排编译。
  3. 编译器线程与应用程序线程并行工作。同时该方法继续在解释器中执行。
  4. 一旦编译器线程完成编译,方法入口点就会被替换,因此下一次调用 f() 将调用该方法的编译版本。

应用程序线程和 JIT 编译器线程之间基本上存在竞争。在方法的编译版本准备好之前,解释器可能会执行不同数量的调用。最后是解释和编译帧的混合。

难怪编译框架布局与解释框架布局不同。编译后的帧通常更小;他们不需要将所有执行上下文存储在堆栈上(方法引用、常量池引用、分析器数据、所有参数、表达式变量等)

此外,Tiered Compilation 还有更多的比赛可能性(自 JDK 8 起默认)。可以有 3 种类型的帧的组合:解释器、C1 和 C2(见下文)。


让我们做一些有趣的实验来支持这个理论。

  1. 纯解释模式。没有 JIT 编译。
    没有比赛 => 稳定的结果。

    $ java -Xint Main
    11895
    11895
    11895
  2. 禁用后台编译。 JIT 已开启,但与应用程序线程同步。
    不再有比赛,但由于编译帧,调用次数现在更高。

    $ java -XX:-BackgroundCompilation Main
    23462
    23462
    23462
  3. 在执行之前用 C1 编译所有内容。与之前的情况不同,堆栈上不会有解释的帧,因此数量会高一些。

    $ java -Xcomp -XX:TieredStopAtLevel=1 Main
    23720
    23720
    23720
  4. 现在用 C2执行之前编译所有东西。这将产生具有最小帧的最优化代码。调用次数最多。

    $ java -Xcomp -XX:-TieredCompilation Main
    59300
    59300
    59300

    由于默认堆栈大小为 1M,这应该意味着现在的帧只有 16 个字节长。是吗?

    $ 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

    其实这里的帧是32字节,但是JIT已经内联了一层递归。

  5. 最后,让我们看看混合堆栈跟踪。为了得到它,我们将在 StackOverflowError 上使 JVM 崩溃(调试版本中可用的选项)。

    $ java -XX:AbortVMOnException=java.lang.StackOverflowError Main

    故障转储 hs_err_pid.log 包含详细的堆栈跟踪,我们可以在其中找到底部的解释帧、中间的 C1 帧和顶部的 C2 帧。

    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

关于java - 为什么导致 StackOverflowError 的递归方法的调用次数在程序运行之间会有所不同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35517934/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com