当前位置: 首页 > 面试题库 >

为什么在程序运行之间,导致递归方法的调用计数不同?

邹野
2023-03-14
问题内容

一个简单的用于演示目的的类:

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编译器线程之间基本上存在竞争。在方法的编译版本准备就绪之前,解释器可能会执行不同数量的调用。最后是解释帧和编译帧的混合。

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

此外,分层编译(JDK 8之后的默认设置)具有更多的竞赛可能性。可以有3种类型的帧组合:解释器C1和C2(请参见下文)。

让我们进行一些有趣的实验以支持该理论。

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

    $ java -Xint Main
    

    11895
    11895
    11895

  2. 禁用 后台 编译。JIT为ON,但与应用程序线程同步。
    再也没有比赛,但是由于已编译的帧,调用的数量现在更多了。

    $ 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内联了一级递归。

  1. 最后,让我们看一下混合堆栈跟踪。为了获得它,我们将使JVM在StackOverflowError上崩溃(调试版本中可用的选项)。
    $ 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


 类似资料:
  • 我编写了以下代码来实现BST的递归插入方法。但是当我以遍历顺序打印树时,它会在插入之前打印原始树。似乎没有插入元素。请帮帮我。提前谢谢。另外,请建议更改代码。顺便说一下,初始树的遍历顺序是2 5 6 7 8。

  • 问题内容: 我对如何做一些我认为很简单的事情感到困惑。我有一个使用编写的简单应用。看起来像这样: 我发现我的终端以其他Flask应用程序的其他调试代码输出了打印语句,但没有输出。如果我在app.run之前删除呼叫,则输出正常。此外,我发现启动时会重复输出两次,尽管我不知道这是不是很奇怪的输出,或者该函数实际上被调用了两次。 我假设这不是在调用之前添加函数调用的正确方法。我查看了Flask文档,发现

  • 本文向大家介绍Python计算程序运行时间的方法,包括了Python计算程序运行时间的方法的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了Python计算程序运行时间的方法。分享给大家供大家参考。具体实现方法如下: 希望本文所述对大家的Python程序设计有所帮助。

  • 我尝试通过使用ThreadPoolExecutor调用在单独线程中执行DNS查询的所有例程来实现异步DNS解析器。 我对可调用对象的定义如下: 基本上,可调用对象将尝试将主机名解析为InetAddress。 然后我定义一个ExecutorService: 我提交了可调用任务: 然后我看着我的程序抛出的日志,它们很奇怪。这是我在解决DNS中有一个超时: 因此,在提交我的可调用对象之后,但在调用fut

  • 我遇到了一种奇怪的行为。我的应用程序有一个主要活动,由navdrawer布局和默认列表片段组成。列表片段使用改造客户端获取内容。选择列表项时,将显示详细视图。在小屏幕上,通过单独的活动加载详细信息视图。该活动还使用改装客户端获取详细信息。然后,可以从生成新活动的navdrawer中选择一个选项。该活动也使用改造客户端发送数据。 列表和详细活动很好地调用了改造方法。我可以看到日志和事情正在按预期工作