例如,一个方法中有10000次的循环。当它运行1000次时,backedge_counter触发JIT
编译。解释器继续执行。当它循环4000次时,JIT
编译完成。
我的问题是,剩下的 6000次是如何被解释器执行的,还是执行本机代码?还是直到下次调用此方法才执行本机代码?下次调用此方法时会发生什么?
假设您询问的是 HotSpot JVM,答案是剩余的交互将在编译后的代码中执行。
HotSpot JVM 有一种称为“堆栈替换”的技术,可以在方法运行时从解释器切换到编译代码。
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
堆栈上替换
也称为“OSR”。将解释的(或优化程度较低的)堆栈帧转换为编译的(或更优化的)堆栈帧的过程。当解释器发现一个方法正在循环时,就会发生这种情况,请求编译器生成一个特殊的 nmethod,其入口点位于循环中的某处(特别是在后向分支处),并将控制权转移给该 nmethod。去优化的粗略逆。
如果您使用-XX:+PrintCompilation
标志运行 JVM,OSR 编译将用%
符号标记:
274 27 3 java.lang.String::lastIndexOf (52 bytes)
275 29 3 java.lang.String::startsWith (72 bytes)
275 28 3 java.lang.String::startsWith (7 bytes)
275 30 3 java.util.Arrays::copyOf (19 bytes)
276 32 4 java.lang.AbstractStringBuilder::append (29 bytes)
276 31 s 3 java.lang.StringBuffer::append (13 bytes)
283 33 % 3 LoopTest::myLongLoop @ 13 (43 bytes)
^ ^
OSR bytecode index of OSR entry
更新
通常,在 OSR 编译之后,常规编译也会排队,以便下次调用该方法时,它将直接以编译模式开始运行。
187 32 % 3 LoopTest::myLongLoop @ 13 (43 bytes)
187 33 3 LoopTest::myLongLoop (43 bytes)
但是,如果在再次调用该方法时常规编译尚未完成,则该方法将开始在解释器中运行,然后将在循环内切换到 OSR 条目。
让我们重申一下这个问题:
Java HotSpot 编译器是否能够在执行过程中将方法从解释型更改为编译型?
我认为可以。
这项任务对引擎来说并不容易(几年前,我在为 PalmOS 手持设备开发名为JUMP的 Ahead-of-Time 编译器时收集了该领域的一些经验)。当引擎决定切换时,它必须至少考虑以下几点:
程序计数器在哪里?在解释代码中,它位于方法开头的某个字节码偏移处,确切地知道接下来要执行哪个字节码。在优化的本机代码中,通常 JVM 字节码不会转换为孤立的机器指令块,而是相互依赖、无序重新排列等等。所以切换时可能没有与字节码程序计数器完全对应的本机指令地址。
数据在哪里?解释器(可能)将所有内容都保存在堆栈上,优化的本机代码使用寄存器和堆栈分配的混合,这对于本机翻译的不同位置会有所不同。
所以我阅读了 HotSpot 白皮书。它没有明确回答这个问题,但在“去优化”下有一个提示。当一个新类被动态加载甚至在调试会话中被替换时,以前的优化(如内联)可能会变得无效。
因此,Java HotSpot VM 必须能够动态地取消优化(然后在必要时重新优化)先前优化的热点,即使在执行热点代码时也是如此。
这也是在编译代码和解释代码之间切换,只是反过来。由于这是 HotSpot 引擎的记录行为,我得出的结论是,在当前执行的方法调用中从解释代码切换到编译代码是可能的。
编辑:
我对我理解为问题核心的内容不够明确。
我知道有一种方法可以进行 10000 次迭代的循环,如下所示:
void loop() {
for (int i=0; i<10000; i++) {
// example loop body
objects[i].doSomething();
}
}
例如,经过 4000 次迭代后,HotSpot 编译器优化了该方法。那会发生什么?
有两个方面,一是琐碎的,一是复杂的:
微不足道的一点是,在循环内发生的调用(例如doSomething()
)将在可用时立即调用它们的编译版本。我在最初的答案中没有提到这一点,因为我认为这是理所当然的。
复杂的方面是:当前运行的loop()
执行是否会在 i=4000 时从解释代码切换到编译代码?这就是我理解为OP的问题。