即时编译
编译器和解释器
静态编译(static compilation):也叫 事前编译(ahead-of-time compilation, AOT),在程序运行前将所有代码编译成 机器码。
动态编译(dynamic compilation):与静态编译完全相反,只在程序运行时才进行编译,编译的结果不会被保留。
即时编译(just-in-time compilation):在程序运行期间,如果遇到 热点代码块,将其编译成 机器码,这样下次遇到这个代码块可以直接执行机器码,而不用重新编译。即时编译是一种特殊的动态编译,即在纯动态编译基础上对 热点代码块 做了编译后机器码缓存的优化。
解释器:直接执行代码得到结果,适用于需要快速启动和执行的程序。
编译器:将代码编译成机器码,执行效率更高,但是需要额外的编译时间。
在 JIT 中,编译器需要额外的编译时间,编译的结果需要额外存放在内存里。
编译的时间开销
解释器 可以抽象为:代码 → [解释器] → 结果
,
JIT编译器 可以抽象为:代码 → [JIT编译器] → 机器码 → [执行] → 结果
。
JIT编译执行比解释器执行多了一个步骤,因此整体上JIT编译 慢于 解释器执行,但是对于 热点代码块,一旦进行JIT编译,之后再次执行都是直接执行的编译后的 机器码,其速度要 快于 直接用解释器执行。因此,只有对需要频繁执行的代码,JIT编译才能保证有正面的收益。
编译的空间开销
使用 javac
可以将 java 源码编译成 字节码,文件后缀为 .class
。
字节码能够直接被 java虚拟机 读取并执行,是一种与平台无关的二进制数据。
非java家族的语言可以将自己的代码转换为字节码,从而在java虚拟机上执行。
在JIT编译中,如果将 字节码 完全编译成 机器码,其体积会显著变大,会大大消耗内存。因此 JVM 总是将 解释器 与 JIT编译器 结合起来使用。
什么是热点代码
热点代码 是指在程序运行过程中会被多次执行的代码:
- 被多次调用的 函数 或者 方法
- 被多次执行的循环体
栈上替换(On Stack Replacement, OSR):编译发生在方法执行的过程中,此时 方法栈帧 还在栈上,但是方法的内容被替换掉了。
热点代码的判定
需要有一种标准用于在程序运行过程中判断哪些代码是热点代码,从而触发 JIT编译,即 热点探测(Hot Spot Detection)。热点探测的主要方式有:
- 周期性的检查各个线程的栈顶,经常出现在栈顶的方法即为热点方法
- 优点:简单高效;通过展开堆栈可以很容易获取方法的调用关系
- 缺点:很难精确的量化方法的热度,因为线程阻塞或者外界环境波动会影响方法热度的判定
- 为每个方法建立 计数器,统计方法的执行次数
- 优点:统计结果更加精确和严谨
- 缺点:实现方式更加复杂;不能获取到方法的调用关系
HotSpot 虚拟机 使用的是 计数器判定法,它为每个方法准备了两个计数器,当计数器之和达到某个阈值时触发 JIT编译:
- 方法调用计数器:统计方法被调用的次数
- 回边计数器:统计方法中 循环体代码块 执行的次数,回边 指的是 字节码 中遇到控制流向后跳转的指令
当 计数器之和达到设定的阈值 时,虚拟机会向 JIT编译器 提交一个编译请求,这个请求是异步的,虚拟机会以 解释执行 的方式继续执行,直到下一次再次执行该代码时,会检查该方法是否已经被编译。
编译的两种模式
HotSpot虚拟机 提供了两种编译模式:
- Client Compiler:注重于局部优化,简单快速
- Server Compiler:注重于全局优化,耗时,面向服务器
评论区