即时编译(Just-In-Time Compiler,JIT)
在HotSpot虚拟机中,java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁,就会认定这些代码为热点代码(Hot spot Code),虚拟机将会把这些代码编译成本地机器码,并以各种手段进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器
触发条件
- 被多次调用的方法
- 被多次执行的循环体
对与上述两种情况,编译的目标对象都是整个方法体。针对后一种情况,编译器仍以整个方法未编译对象,只是执行入(从方法第几条字节码指令开始执行)会稍有不同,编译时会传入执行入口点字节码序号(byte code index,BCI)。这种编译方式因为编译发生在方法执行的过程中,因此被称为"栈上替换"(on stack replacement,OSR),即方法的栈帧还在栈上,方法就被替换了。
热点探测(hot spot code detection)
HotSpot采用基于计数器的热点探测方法,为每个方法准备了两类计数器:
-
方法调用计数器(invocation counter)
在垃圾收集或指定-XX:CounterHalfLifeTime时间内,统计次数会减少一半,也可以通过-XX:-UseCounterDecay来关闭热度衰减,这样只要系统运行时间长绝大部分方法都会被编译成本地代码。
-
回边计数器(back edge counter)
回边:在字节码遇到控制流向后跳转的指令称为回边,通常在循环语句中出现,如引用并修改了循环外变量、continue等。
解释器遇到一条回边指令时,会查找将要执行的代码片段是否已有编译好的版本,如果有会优先执行已编译的代码,否则回边计数+,然后判断方法调用计数器与回边计数器值之和是否超过回边计数的阈值。超过阈值则提交一个栈上替换编译请求。
方法内联 (method inlining)
将目标方法的代码复制到发起调用的方法之中,可去除方法的调用成本(查找方法、开辟栈帧),同时方法内联膨胀后便与在更大范围上进行后续优化。
守护内联(guarded inlining)
由于虚方法在编译期无法确定最终用调用的方法版本,也就是无法做到编译期进行内联优化,java虚拟机引入类型继承关系分析(class hierarchy analysis,CHA)技术。用于确认某个类、接口是否有多于一种实现,某个类是否存在子类,子类是否覆盖了父类的某个虚方法。如果确定某个方法只有一个版本,则假设"应用程序的全貌就是现在运行的这个样子"进行内联,这种内联称为守护内联。虚拟机后续加载到令该方法继承关系发生变化的类时,抛弃编译好的代码,退回到解释状态进行执行。
内联缓存(inline cache)
当CHA查询的结果是该方法有多个版本可选择,即时编译 使用 内联缓存 的方式来减少方法调用开销。在未发生方法调用时,内联缓存为空,第一次发生后,缓存记录方法接受者的版本信息,每次进行方法调用时都比较接收者的版本,如果一致,那就是单态内联缓存,可以省去方法表查询比较,仅多一次类型判断开销。如果出现不一致的情况,则说明程序用到了虚方法的多态特性,退化成超多态内联缓存,开销相当于查虚方法表进行分派。为了节省内存空间,Java 虚拟机只采用单态内联缓存。
逃逸分析(escape analysis)
分析对象动态作用域,当一个对象在方法里被定义后,作为调用参数传递到其他方法中,称为方法逃逸。能被其他外部线程访问则称为线程逃逸。当能证明一个对象不会逃逸到其他方法、线程之外时或逃逸程度较低(只逃逸出方法不会逃逸出线程)则可以进行相应优化:
-
栈上分配(stack allocations)
当一个对象不会逃逸出线程之外,那么可以让这个对象在栈上分配内存,对象所占用的内存随栈帧出栈而销毁。可以减轻垃圾收集系统标记、回收甚至整理的工作量。栈上分配允许对象逃逸出方法,不支持线程逃逸。
-
标量替换(scalar replacement)
若一个数据无法分解为更小的数据来表示,如int、long等,那么这些数据就可以称为标量。如果可以继续分解,则称之为聚合量。标量替换就是检测到java对象不会逃逸出方法、线程外时,将对象拆散,程序执行的时候可能不会取创建这个对象,而是创建若干个被这个方法使用的成员变量代替。这样的好处是可以让对象的成员变量在栈上分配(栈上存储的数据很大机会被虚拟机分配至物理机器的高速寄存器中存储)和读写外,还可以为后续进一步优化创建条件。标量替换不支持方法逃逸、线程逃逸。
-
同步消除(snchronization elimination)
线程同步本身是一个相对耗时的过程,如果能确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就不会发生竞争,同步措施也可以安全地消除掉。
公共子表达式消除
如果一个表达式E之前就已经计算过了,并且从之前的计算到现在E中所有的变量都未发生变化, 那么E的这次出现就称为公共子表达式。如 int d = ab + 2+ ab经过即时编译后可能优化为 int d = E + 2 + E。