java 及时编译_JVM进阶 -- 浅谈即时编译

即时编译技术提升Java应用运行效率,通过解释执行到机器码的转变。HotSpot包含C1、C2和Graal编译器,Java 7引入分层编译,结合C1和C2优势。C2编译效率通常高于C1约30%,分层编译的5个层次各有作用。JVM依据方法调用次数和循环回边执行次数触发JIT,C1和C2的阈值不同。编译线程数根据CPU核心动态调整,通过参数控制编译层次和优化策略。
摘要由CSDN通过智能技术生成

即时编译是用来 提升应用运行效率 的技术

代码会先在JVM上 解释执行 ,之后反复执行的 热点代码 会被 即时翻译成为机器码 ,直接运行在 底层硬件 上

分层编译

HotSpot包含多个即时编译器:C1、C2和Graal(Java 10,实验性)

在 Java 7之前,需要根据程序的特性选择对应的 即时编译器

对于 执行时间较短 或 对启动性能有要求 的程序,采用 编译效率较快的C1 ,对应参数: -client

对于 执行时间较长 或 对峰值性能有要求 的程序,采用 生成代码执行效率较快的C2 ,对应参数: -server

Java 7引入了 分层编译 (-XX:+TieredCompilation),综合了 C1的启动性能优势 和 C2的峰值性能优势

分层编译将 JVM的执行状态 分了5个层次

0:解释执行(也会profiling)

1:执行 不带profiling 的C1代码

2:执行仅带 方法调用次数 和 循环回边执行次数 profiling的C1代码

3:执行带 所有profiling 的C1代码

4:执行C2代码

通常情况下, C2代码的执行效率比C1代码高出30%以上

对于C1代码的三种状态,按执行效率从高至低:1层 > 2层 > 3层

1层的性能略高于2层,2层的性能比3层高出30%

profiling越多,额外的性能开销越大

profiling:在程序执行过程中,收集能够反映程序执行状态的数据

profile:收集的数据

JDK附带的 hprof (CPU+Heap)

JVM 内置profiling

Java 8默认开启了分层编译,无论开启还是关闭分层编译,原本的 -client 和 -client 都是无效的

如果 关闭分层编译 ,JVM将直接采用 C2

如果只想用C1,在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1

编译路径

3dd1673754f3b9a147e6294b3cf677df.png

1层和4层是 终止状态

当一个 方法 被 终止状态 编译后,如果 编译后的代码没有失效 ,那么JVM 不会再次发出该方法的编译请求

通常情况下,热点方法会被3层的C1编译,然后再被4层的C2编译

如果方法的 字节码数目较少 (如getter/setter),并且 3层的profiling没有可收集的数据

JVM会断定 该方法对于C1和C2的执行效率相同

JVM会在3层的C1编译后, 直接选用1层的C1编译

由于1层是 终止状态 ,JVM不会继续用4层的C2编译

在C1忙碌的情况下,JVM在 解释执行过程 中对程序进行 profiling ,而后直接由4层的C2编译

在C2忙碌的情况下,方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间

触发JIT的条件

JVM是依据 方法的调用次数 以及 循环回边的执行次数 来触发JIT的

JVM将在0层、2层和3层执行状态时进行profiling,其中包括方法的调用次数和循环回边的执行次数

循环回边是一个控制流程图中的概念,在字节码中,可以简单理解为 往回跳 的指令

在即时编译过程中,JVM会识别循环的头部和尾部, 循环尾部到循环头部的控制流就是真正意义上的循环回边

C1将在 循环回边 插入 循环回边计数器 的代码

解释执行和C1代码中增加循环回边计数的 位置 并不相同,但这不会对程序造成影响

JVM不会对这些 计数器 进行 同步 操作,因此收集到的执行次数也 不是精确值

只要该数值 足够大 ,就能表示对应的方法包含热点代码

在 不启动 分层编译时,当 方法的调用次数和循环回边的次数的和 超过-XX:CompileThreshold,便会触发JIT

使用 C1 时,该值为 1500

使用 C2 时,该值为 10000

当 启用 分层编译时,阈值大小是 动态调整 的

阈值 * 系数

系数

系数的计算方法:

s = queue_size_X / (TierXLoadFeedback * compiler_count_X) + 1

其中X是执行层次,可取3或者4

queue_size_X:执行层次为X的待编译方法的数目

TierXLoadFeedback:预设好的参数,其中Tier3LoadFeedback为5,Tier4LoadFeedback为3

compiler_count_X:层次X的编译线程数目。

编译线程数

在64位JVM中,默认情况下,编译线程的总数目是根据 处理器数量 来调整的

-XX:+CICompilerCountPerCPU=true, 编译线程数依赖于处理器数量

-XX:+CICompilerCountPerCPU=false -XX:+CICompilerCount=N, 强制设定总编译线程数

JVM会将这些编译线程按照1:2的比例分配给C1和C2(至少1个),对于4核CPU,总编译线程数为3

// -XX:+CICompilerCountPerCPU=true

n = log2(N) * log2(log2(N)) * 3 / 2

其中 N 为 CPU 核心数目,N >= 4

触发条件

当启用分层编译时,触发JIT的条件

i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)

其中i为方法调用次数,b为循环回边执行次数

Profiling

在分层编译中的0层、2层和3层,都会进行profiling,最为基础的是 方法的调用次数 以及 循环回边的执行次数

主要拥有触发JIT

此外,0层和3层还会收集用于4层C2编译的数据,例如

branch profiling

分支跳转字节码,包括跳转次数和不跳转次数

type profiling

非私有实例方法调用指令: invokevirtual

强制类型转换指令: checkcast

类型测试指令: instanceof

引用类型数组存储指令: aastore

branch profiling和type profiling将给应用带来不少的 性能开销

3层C1的性能比2层C1的性能低30%

通常情况下,我们不会在 解析执行 过程中进行branch profiling和type profiling

只有在方法 触发C1编译后 ,JVM认为该方法 有可能被C2编译 ,才会在该方法的C1代码中收集这些profile

只有在 极端 情况下(如等待C1编译的方法数目太多),才会开始在 解释 执行过程中收集这些profile

C2可以根据收集得到的数据进行 猜测和假设 ,从而作出比较 激进的优化

branch profiling

Java代码

public static int foo(boolean f, int in) {

int v;

if (f) {

v = in;

} else {

v = (int) Math.sin(in);

}

if (v == in) {

return 0;

} else {

return (int) Math.cos(v);

}

}

字节码

public static int foo(boolean, int);

descriptor: (ZI)I

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=2, locals=3, args_size=2

0: iload_0

1: ifeq 9 // false,跳转到偏移量为9的字节码

4: iload_1

5: istore_2

6: goto 16

9: iload_1

10: i2d

11: invokestatic // Method java/lang/Math.sin:(D)D

14: d2i

15: istore_2

16: iload_2

17: iload_1

18: if_icmpne 23 // 如果v!=in,跳转到偏移量为23的字节码

21: iconst_0

22: ireturn

23: iload_2

24: i2d

25: invokestatic // Method java/lang/Math.cos:(D)D

28: d2i

29: ireturn

优化过程

正常分支

5c5882d666a3f1ab8b8bf3d26db4d028.png

profiling

假设应用程序调用该方法,所传入的都是true,那么偏移量为1和偏移量为18的条件跳转指令所对应的分支profile中,其跳转的次数都是0。实际执行的分支如下:

ba9cf3167783e635caec322a38d2f90e.png

剪枝

C2根据这两个分支profile作出假设,在后续的执行过程中,这两个条件跳转指令仍旧不会执行,基于这个假设,C2不会在编译这两个条件跳转语句所对应的false分支(剪枝)。最终的结果是在第一个条件跳转之后,C2代码直接返回0

acd55aa2feb8bbcf0ecf8f05ded78613.png

小结

根据条件跳转指令的分支profile,即时编译器可以将 从未执行过 的分支减掉

避免编译这些不会用到的代码

节省 编译时间 以及部署代码所要消耗的 内存空间

剪枝同时也能精简数据流,从而触发更多的优化

现实中,分支profile出现仅跳转或者不跳转的情况并不常见

即时编译器对分支profile的利用也不仅仅限于剪枝

还可以依据分支profile, 计算每一条执行路径的概率

以便于某些编译器优化优先处理概率较高的路径

type profiling

Java代码

public static int hash(Object in) {

if (in instanceof Exception) {

return System.identityHashCode(in);

} else {

return in.hashCode();

}

}

字节码

public static int hash(java.lang.Object);

descriptor: (Ljava/lang/Object;)I

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: instanceof // class java/lang/Exception

4: ifeq 12 // 不是Exception,跳转到偏移量为12的字节码

7: aload_0

8: invokestatic // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I

11: ireturn

12: aload_0

13: invokevirtual // Method java/lang/Object.hashCode:()I

16: ireturn

优化过程

正常分支

17ac8ad0dd921e03dd0392e2e0ccfa21.png

profiling+优化

假设应用调用该方法时,所传入的Object皆为Integer实例

偏移量为1的 instanceof 指令的 类型profile 仅包含Integer

偏移量为4的分支跳转语句的 分支profile 不跳转次数为0

偏移量为13的方法调用指令的 类型profile 仅包含Integer

测试instanceof

如果instanceof的 目标类型是final类型 ,那么JVM仅需比较测试 对象的动态类型 是否为该final类型

如果 目标类型不是final类型 ,JVM需要依次按下列顺序测试是否与目标类型一致

该类本身

该类的父类、祖先类

该类所直接实现或间接实现的接口

instanceof指令的类型profile仅包含Integer

JVM会假设在接下来的执行过程中,所输入的Object对象仍为Integer对象

生成的代码将 直接测试所输入的动态类型是否为Integer ,如果是继续执行接下来的代码

然后,即时编译器会采用 针对分支profile的优化 以及 对方法调用的条件去虚化内联

内联结果:生成的代码将测试所输入对象的动态类型是否为Integer,如果是,执行 Integer.hashCode() 方法的代码

public final class Integer ... {

@Override

public int hashCode() {

return Integer.hashCode(value);

}

public static int hashCode(int value) {

return value;

}

}

针对上面三个profile的分支图

d04eecfc0fb47ee083d4a79ab3620625.png

进一步优化(剪枝)

2619c41189b2e24e24e15a56539c4a9d.png

小结

和基于分支profile的优化一样,基于类型profile的优化同样也是作出假设,从而精简控制流以及数据流,两者的 核心是假设

对于 分支profile ,即时编译器假设 仅执行某一分支

对于 类型profile ,即时编译器假设的是 对象的动态类型仅为类型profile中的那几个

如果 假设失败 ,将进入 去优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值