vlambda博客
学习文章列表

大牛用一文带你深入解析java虚拟机:C1编译器的编译流程



编译流程

本节从源码出发,简单介绍C1的中间表示和编译流程。后续小节将详细描述这些过程。

进入C1

当解释器发现热点方法时会调用
CompilerBroker::comple_method()向编译任务队列投递一个编译任务(Compile Task),C1编译器线程发现队列有编译任务时会被唤醒,并拉取编译任务然后进入JIT编译器的世界。目光转向C1编译线程(C1 Compiler Thread),它最开始阻塞在编译任务队列,发现编译任务后被唤醒,经过代码清单8-1所示调用链后开始编译。


代码清单8-1 C1调用链

JavaThread::thread_main_entry()
-> compiler_thread_entry()
-> CompilerBroker::compiler_thread_loop()
-> CompileBroker::invoke_compiler_on_method() // 使用C1
-> Compiler::compile_method() // 进入C1世界
-> Compilation::Compilation() // 代码编译
-> Compilation::compile_method()
-> Compilation::compile_java_method()

C1的完整编译周期等价于Compilation对象的构造周期,
Compilation::compile_method包含编译代码和安装编译后代码两个动作,Compilation::compile_java_method表示编译动作,阅读C1的源码可以从这里入手,如代码清单8-2所示。

代码清单8-2
Compilation::compile_java_method

int Compilation::compile_java_method() {
{ // 构造HIR
PhaseTraceTime timeit(_t_buildIR);
build_hir();
}
{ // 构造LIR
PhaseTraceTime timeit(_t_emit_lir);
_frame_map = new FrameMap(...);
emit_lir();
}
{ // 生成机器代码
PhaseTraceTime timeit(_t_codeemit);
return emit_code_body();
}
}

C1将Java字节码转换为各种形式的中间表示,然后在其上做代码优化和机器代码生成,这个机器代码就是C1的产出。可以看出,连通Java字节码和JIT产出的机器代码的桥梁就是中间表示,C1的大部分工作也是针对中间表示做各种变换。

有一个取巧的办法可以得到C1详细的工作流程:C1会对编译过程中的每个小阶段做性能计时,这个计时取名就是阶段名字,所以可以通过计时查看详细步骤,如代码清单8-3所示。

代码清单8-3 C1编译详细流程

typedef enum {
_t_compile, // C1编译
_t_setup, // 1)设置C1编译环境
_t_buildIR, // 2)构造HIR
_t_hir_parse, // 从字节码生成HIR
_t_gvn, // GVN优化
_t_optimize_blocks, // 基本块优化
_t_optimize_null_checks, // null检查优化消除
_t_rangeCheckElimination, // 数组范围检查消除
_t_emit_lir, // 3)构造LIR
_t_linearScan, // 线性扫描寄存器分配
_t_lirGeneration, // 生成LIR
_t_codeemit, // 机器代码生成
_t_codeinstall, // 将生成的本地代码放入nmethod
max_phase_timers
} TimerName;

总地来说,C1的编译流程是:字节码解析生成HIR→HIR优化→HIR生成LIR→线性扫描寄存器分配机器代码生成设置机器代码。

高级中间表示

开发者用Java写代码,经过javac编译得到相对紧凑、简洁的字节码,但是即便是字节码,对于编译器来说也还是太过高级,所以编译器会使用一种更适合编译优化的形式来表征字节码,这个更适合优化的形式即高级中间表示(HIR)。


HIR是由基本块构成的控制流图,基本块内部是SSA形式的指令序列。第二阶段的build_hir()不仅会构造出HIR,还会执行很多平台无关的代码优化,如代码清单8-4所示。

代码清单8-4 构造HIR

void Compilation::build_hir() {
...
// 创建HIR
{
PhaseTraceTime timeit(_t_hir_parse);
_hir = new IR(this, method(), osr_bci());
}
...
// 优化:条件表达式消除,基本块消除
if (UseC1Optimizations) {
NEEDS_CLEANUPPhaseTraceTime timeit(_t_optimize_blocks);
_hir->optimize_blocks();
}
...
// 优化:全局值编号优化
if (UseGlobalValueNumbering) {
PhaseTraceTime timeit(_t_gvn);
int instructions = Instruction::number_of_instructions();
GlobalValueNumbering gvn(_hir);
}
// 优化:范围检查消除
if (RangeCheckElimination) {
if (_hir->osr_entry() == NULL) {
PhaseTraceTime timeit(_t_rangeCheckElimination);
RangeCheckElimination::eliminate(_hir);
}
}
// 优化:NULL检查消除
if (UseC1Optimizations) {
NEEDS_CLEANUP
PhaseTraceTime timeit(_t_optimize_null_checks)
;
_hir->eliminate_null_checks();
}
}

build_hir()在第一阶段解析字节码生成HIR[1];之后会检查HIR是否有效,如果无效,会发生编译脱离,此时编译器停止编译,然后回退到解释器。当对HIR的检查通过后,C1会对其进行条件表达式消除,基本块消除;接着使用GVN后再消除一些数组范围检查;最后做NULL检查消除。另外要注意的是,如果使用-XX:+TieredCompilation开启了分层编译,那么条件表达式消除和基本块消除只会发生在分层编译的1、2层。

低级中间表示

高级中间表示屏蔽了具体架构的细节,使得优化更加方便。当这些优化完成后,为了贴近具体架构,还需要将高级中间表示转换为低级中间表示(LIR),然后基于LIR进行寄存器分配,如代码清单8-5所示。


代码清单8-5 emit_lir

void Compilation::emit_lir() {
{ // HIR转换为LIR
PhaseTraceTime timeit(_t_lirGeneration);
hir()->iterate_linear_scan_order(&gen);
}
{ // 寄存器分配。将LIR的虚拟寄存器映射到物理寄存器
PhaseTraceTime timeit(_t_linearScan);
LinearScan* allocator = new LinearScan(hir(), &gen, frame_map());
allocator->do_linear_scan();
...
}}

首先使用LIRGenerator将HIR转换为更低级的LIR,然后使用LinearScan根据线性寄存器分配算法将LIR中的虚拟寄存器映射到指令集架构允许的物理寄存器上。一个直观的HIR表示可以参见代码清单8-6,它表示一个简单的a+b的加法操作,其中a和b是方法参数。

代码清单8-6 加法的HIR

B1 -> B0 [0, 0]
Locals size 3 [static jint AddTest.add(jint, jint)]
0 i1 [method parameter]
1 i2 [method parameter]
_p__bci__use__tid__instruction________________________ (HIR)
. 0 0 v6 std entry B0
B0 <- B1 [0, 5] std
Locals size 3 [static jint AddTest.add(jint, jint)]
0 i1
1 i2
_p__bci__use__tid__instruction________________________ (HIR)
2 0 i3 i1 + i2
. 5 0 i4 ireturn i3

当完成HIR转LIR以及寄存器分配之后,生成的LIR如代码清单8-7所示。

代码清单8-7 加法的LIR

B1 -> B0 [0, 0]
_nr__instruction______________________(LIR)
0 label [label:0x0000000125245ea0]
2 std_entry
B0 <- B1 dom B1 [0, 5] std
_nr__instruction______________________(LIR)
10 label [label:0x00000001252451d0]
14 add [rsi|I] [rdx|I] [rsi|I]
16 move [rsi|I] [rax|I]
18 return [rax|I]

[rsi|I]表示使用物理寄存器rsi存放int值。类似的还有[R10|L],表示虚拟寄存器R10存放long值。[stack:0|I]表示栈的第0个槽存放int值。

[int:1|I]表示int常量1。

当LIR完成寄存器分配后,
Compilation::emit_code_body()会将LIR代码转化为机器代码,emit_code_body()会将任务最终委托给LIR_Assembler。由于LIR代码近似于指令集表示,所以机器代码生成的过程可看作线性映射的过程,一些高级的LIR代码除外,因为这些需要更多的汇编模拟。

本文给大家讲解的内容是深入解析java虚拟机:C1编译器,编译流程

  1. 下篇文章给大家讲解的是深入解析java虚拟机:C1编译器,从字节码到HIR;

  2. 觉得文章不错的朋友可以转发此文关注小编;

  3. 感谢大家的支持!