即时编译器
HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器。Java8默认开启Server模式。用户可以使用“-client”或“-server”参数去指定编译模式。
C1编译器启动速度快,关注局部简单可靠的优化,比如方法内联、常量传播等。C2编译器 关注一些编译耗时较长的全局优化,甚至会根据性能监控(profiling)进行一些不可靠的激进优化。它的性能通常比 C1编译器 高30%以上,适用于长时间运行的后台程序。
C1和 C2编译器是由C++写成,目前还有用Java 编写的即时编译器Graal。
Java 7 引入了分层编译的概念,综合了C1的启动性能优势和 C2 的峰值性能优势。分层编译主要分为如下层级:
1)解释执行。
2)执行C1代码,根据运行情况进行profiling。
3)执行C2代码,根据profiling进行激进优化。
profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据 。profiling越多,其额外的性能开销越大。其中最基本的统计数据是方法的调用次数以及循环回边的执行次数,用于判断热点代码,并触发即时编译。计数默认阈值在Client模式下是1500次,在Server模式下是10000次。
方法调用计数器
方法调用计数器(Invocation Counter),顾名思义,这个计数器就是用于统计方法被调用的次数。需注意该 计数器统计的非绝对次数,而是一个相对的执行频率。当超过一定的时间限度,如果方法的调用次数仍不足以触发即时编译,那这个方法的调用计数会被减少一半,这个过程称为热度的衰减 (Counter Decay),而这段时间就称为此方法统计的半衰周期 (Counter Half Life Time)。
// 假设input这个Http接口单位时间内调用doSomething方法一万次以上
@RequestMapping(value = "/input")
CommonResponse input(@RequestBody InputRequest request){// 将doSomething进行方法内联编译优化return CommonResponse.ok(doSomething(request));
}void doSomething() {// 将当前代码编译成本地机器码......
}
循环回边计数器
void loop() {int sum = 0;for (int i = 0; i < 10; i++) {sum += i;}
}
上面这段代码经过编译生成下面的字节码:
public void loop();Code:0: iconst_01: istore_12: iconst_03: istore_24: iload_25: bipush 107: if_icmpge 2010: iload_111: iload_212: iadd13: istore_114: iinc 2, 117: goto 420: return
在上述字节码中,循环回边计数器被存储在第7行的if_icmpge指令中。if_icmpge指令用于接收两个操作数用于比较计算,以决定循环体 跳转 的位置。在解释执行时,每当运行一次该指令,该方法的循环回边计数器加1。
循环回边计数器(Loop BackEdge Counter)触发的优化技术 叫作栈上替换 (On Stack Replacement , OSR) 。假设有 一个 方法 只被 调用一次,但却包含超过一万次以上循环迭代次数,这个循环方法无法以方法调用计数来统计。而 栈上替换技术 解决了这个问题。当编译器检测到一个循环已经迭代足够次数,它会将循环中的代码动态编译成机器代码,并在适当的时机进行切换。
void largeLoop() {// 假设largeLoop是一个只被调用一次,但包含100百万次循环迭代// 1)循环回边计数器通过迭代计数统计,触发即时编译// 2)将循环中的代码编译成本地机器码for (int i = 0; i < 1000000; i++) {......}
}
提前编译器
提前 编译器 (Ahead Of Time, AOT ),是与即时编译器相对立的一个概念,指在程序运行之前将字节码转换为本地机器码,从而减少了运行时的编译开销, 提高启动速度。属于一种静态编译手段。
但Java 语言本身的动态特性带来了额外的复杂性,影响了程序静态编译代码的质量。例如 Java 语言的运行时动态类加载,因为 提前 编译器 是在程序运行前进行编译的,所以无法获知这一信息。