为什么 JVM 不用 JIT 全程编译
从知乎扣出来的内容 https://www.zhihu.com/question/37389356
链接:https://www.zhihu.com/question/37389356/answer/73820511
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。
1. 编译的时间开销
解释器的执行,抽象的看是这样的:输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果
说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。
然而这JIT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。
所以,对“只执行一次”的代码而言,解释执行其实总是比JIT编译执行要快。怎么算是“只执行一次的代码”呢?粗略说,下面两个条件同时满足时就是严格的“只执行一次”
- 只被调用一次,例如类的初始化器(class initializer,<clinit>()V)
- 没有循环
对只执行一次的代码做JIT编译再执行,可以说是得不偿失。
对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。
只有对频繁执行的代码,JIT编译才能保证有正面的收益。
况且,并不是说JIT编译了的代码就一定会比解释执行快。切不可盲目认为有了JIT就可以鄙视解释器了,还是得看实现细节如何。
有个很经典的例子:LuaJIT 2里有一个实现得非常优化的解释器,它解释执行的速度甚至比LuaJIT 1的JIT编译后的代码的速度还要快。
2. 编译的空间开销
举个最简单的例子: public static int foo() {
return 42;
}
public static int foo();
Code:
stack=1, locals=0, args_size=0
0: bipush 42
2: ireturn
# {method} 'foo' '()I' in 'XX'
# [sp+0x20] (sp of caller)
0x00000001017b8200: sub $0x18,%rsp
0x00000001017b8207: mov %rbp,0x10(%rsp) ;*synchronization entry
; - XX::foo@-1 (line 3)
0x00000001017b820c: mov $0x2a,%eax
0x00000001017b8211: add $0x10,%rsp
0x00000001017b8215: pop %rbp
0x00000001017b8216: test %eax,-0x146021c(%rip) # 0x0000000100358000
; {poll_return}
0x00000001017b821c: retq
0x00000001017b821d: hlt
0x00000001017b821e: hlt
0x00000001017b821f: hlt
[Exception Handler]
[Stub Code]
0x00000001017b8220: jmpq 0x00000001017b50a0 ; {no_reloc}
[Deopt Handler Code]
0x00000001017b8225: callq 0x00000001017b822a
0x00000001017b822a: subq $0x5,(%rsp)
0x00000001017b822f: jmpq 0x000000010178eb00 ; {runtime_call}
0x00000001017b8234: hlt
0x00000001017b8235: hlt
0x00000001017b8236: hlt
0x00000001017b8237: hlt
对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10x是很正常的。上面的例子比较极端一些,但还是很能反映现实状况的。
同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致“代码爆炸”(code size explosion)。
3. 编译时机对优化的影响
有些JIT编译器非常简单,基本上不做啥优化,也倒也没啥影响。
但现代做优化的JIT编译器都非常注重使用profile信息,而profile是需要通过执行用户程序来获取的。
这样,编译得太早的话,就来不及收集足够profile信息,进而会影响优化的效果;而编译太迟的话,即便收集了很多高质量的profile,但却也已经付出了profile的额外开销,编译出来的代码再快或许也弥补不过来了。
在解释器里实现收集profile的功能,等解释执行一段时间后再触发JIT编译,这样就可以很好的平衡收集profile与编译优化这两方面。
当然,收集profile也可以在JIT编译器里做:一开始先JIT编译生成收集profile的版本的代码,等收集了到足够profile后触发重新编译,再生成出优化的、不带profile的版本。JRockit基本上就是这样做的。这方面在本回答开头放的链接里已有说明。