Java即时编译器原理解析及实践( 三 )
字节码
public void nlp(java.lang.Object);Code:0: iconst_01: istore_12: iconst_03: istore_24: iload_25: sipush2008: if_icmpge2111: iload_112: iload_213: iadd14: istore_115: iinc2, 118: goto421: return
在即时编译过程中 , 编译器会识别循环的头部和尾部 。 上面这段字节码中 , 循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码 。 编译器将在循环体结尾增加循环回边计数器的代码 , 来对循环进行计数 。
当方法的调用次数和循环回边的次数的和 , 超过由参数-XX:CompileThreshold指定的阈值时(使用C1时 , 默认值为1500;使用C2时 , 默认值为10000) , 就会触发即时编译 。
开启分层编译的情况下 , -XX:CompileThreshold参数设置的阈值将会失效 , 触发编译会由以下的条件来判断:
- 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数 。
- 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数 , 并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时 。
i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s--tt-darkmode-color: #DAA700;">三、编译优化
即时编译器会对正在运行的服务进行一系列的优化 , 包括字节码解析过程中的分析 , 根据编译过程中代码的一些中间形式来做局部优化 , 还会根据程序依赖图进行全局优化 , 最后才会生成机器码 。
1. 中间表达形式(Intermediate Representation)
在编译原理中 , 通常把编译器分为前端和后端 , 前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation , 以下称为IR) , 后端会对IR进行优化 , 生成目标代码 。
Java字节码就是一种IR , 但是字节码的结构复杂 , 字节码这样代码形式的IR也不适合做全局的分析优化 。 现代编译器一般采用图结构的IR , 静态单赋值(Static Single Assignment , SSA)IR是目前比较常用的一种 。 这种IR的特点是每个变量只能被赋值一次 , 而且只有当变量被赋值之后才能使用 。 举个例子:
SSA IR
Plain Text{a = 1;a = 2;b = a;}
上述代码中我们可以轻易地发现a = 1的赋值是冗余的 , 但是编译器不能 。 传统的编译器需要借助数据流分析 , 从后至前依次确认哪些变量的值被覆盖掉 。 不过 , 如果借助了SSA IR , 编译器则可以很容易识别冗余赋值 。
上面代码的SSA IR形式的伪代码可以表示为:
SSA IR
Plain Text{a_1 = 1;a_2 = 2;b_1 = a_2;}
由于SSA IR中每个变量只能赋值一次 , 所以代码中的a在SSA IR中会分成a_1、a_2两个变量来赋值 , 这样编译器就可以很容易通过扫描这些变量来发现a_1的赋值后并没有使用 , 赋值是冗余的 。
除此之外 , SSA IR对其他优化方式也有很大的帮助 , 例如下面这个死代码删除(Dead Code Elimination)的例子:
DeadCodeElimination
public void DeadCodeElimination{int a = 2;int b = 0if(2 > 1){a = 1;} else{b = 2;}add(a,b)}
可以得到SSA IR伪代码:
DeadCodeElimination
a_1 = 2;b_1 = 0if true:a_2 = 1;elseb_2 = 2;add(a,b)
编译器通过执行字节码可以发现 b_2 赋值后不会被使用 , else分支不会被执行 。 经过死代码删除后就可以得到代码:
DeadCodeElimination
public void DeadCodeElimination{int a = 1;int b = 0;add(a,b)}
我们可以将编译器的每一种优化看成一个图优化算法 , 它接收一个IR图 , 并输出经过转换后的IR图 。 编译器优化的过程就是一个个图节点的优化串联起来的 。
C1中的中间表达形式
前文提及C1编译器内部使用高级中间表达形式HIR , 低级中间表达形式LIR来进行各种优化 , 这两种IR都是SSA形式的 。
HIR是由很多基本块(Basic Block)组成的控制流图结构 , 每个块包含很多SSA形式的指令 。 基本块的结构如下图所示:
文章插图
其中 , predecessors表示前驱基本块(由于前驱可能是多个 , 所以是BlockList结构 , 是多个BlockBegin组成的可扩容数组) 。 同样 , successors表示多个后继基本块BlockEnd 。 除了这两部分就是主体块 , 里面包含程序执行的指令和一个next指针 , 指向下一个执行的主体块 。
从字节码到HIR的构造最终调用的是GraphBuilder , GraphBuilder会遍历字节码构造所有代码基本块储存为一个链表结构 , 但是这个时候的基本块只有BlockBegin , 不包括具体的指令 。 第二步GraphBuilder会用一个ValueStack作为操作数栈和局部变量表 , 模拟执行字节码 , 构造出对应的HIR , 填充之前空的基本块 , 这里给出简单字节码块构造HIR的过程示例 , 如下所示:
- 现状|程序员现状揭秘:平均年薪20.36万,Java人才需求量最大
- 工程师|AWS偏爱Rust,已将Rust编译器团队负责人收入囊中
- 程序员学英语第1天——JavaScript 程序测试的介绍1
- 三年Java开发,刚从美团、京东、阿里面试归来,分享个人面经
- 《深入理解Java虚拟机》:对象创建、布局和访问全过程
- java面试题整理
- Kotlin集合vs Kotlin序列与Java流
- Java安全之Javassist动态编程
- 推荐Java工程师必看,12个Hadoop领域的上手项目
- 震惊!京东T4大佬面试整整三个月,才写了两份java面试笔记