版本信息:
jdk版本:jdk8u40
Hotspot中执行引擎分为解释器、JIT及时编译器,上篇文章描述到解释器过度到JIT的条件。JVM源码剖析之达到什么条件进行JIT优化 这篇文章大致讲述JIT的编译过程。在JDK中javac和JIT两部分跟编译原理挂钩,而编译原理抛开内部实现的算法,从思想的角度出发可以分为前端、中间IR、后端,见图1.1描述。比如拿javac举例,开发人员编写好xxx.java 文件,通过javac命令把xxx.java 文件编译成 xxx.class二进制文件交给JVM执行,其中编译的流程就如图1.1的流程。
图1.1 编译原理思想图
而本文论述JIT,所以我们需要把编译原理的思想带入到JIT中,见图1.2,这里的输入为达到JIT编译阈值的方法对应的字节码,经过前端、中间IR语言、后端(这里为了容易理解,把前端、中间IR语言、后端想象成一个编译的黑盒程序)后得到输出结果为ISA指令集(ISA:CPU平台可执行指令)
图1.2 JIT的编译思想图
由图1.3可以得知Java工作线程(Java项目中运行的线程)和编译线程之间通过队列来通信(当然,这也是线程通信的最基本方式),CompileTask作为通信的载体,当Java线程执行某个方法当方法达到JIT优化的阈值或者回边计数器达到了阈值,此时会创建CompileTask投递到CompileQueue队列中。
/*
method_invocation_event
->compile
->submit_compile
->compile_method
->compile_method_base
->create_compile_task
*/
// src/share/vm/compiler/compileBroker.cpp 文件中
CompileTask* CompileBroker::create_compile_task(CompileQueue* queue,
int compile_id,
methodHandle method,
int osr_bci,
int comp_level,
methodHandle hot_method,
int hot_count,
const char* comment,
bool blocking) {
CompileTask* new_task = allocate_task();
new_task->initialize(compile_id, method, osr_bci, comp_level,
hot_method, hot_count, comment,
blocking);
// 给CompilerThread线程对应的队列投递编译任务。
queue->add(new_task);
return new_task;
}
java工作线程给编译线程队列投递任务后,此时编译线程开始干活,接收队列的compileTask任务,开始执行编译工作
// src/share/vm/compiler/compileBroker.cpp 文件中
void CompileBroker::compiler_thread_loop() {
CompilerThread* thread = CompilerThread::current();
// 拿到当前编译线程对应的队列
CompileQueue* queue = thread->queue();
…………
while (!is_compilation_disabled_forever()) {
// 从队列中获取到任务
CompileTask* task = queue->get();
if (task == NULL) {
continue;
}
…………
// 执行任务
invoke_compiler_on_method(task);
}
}
接下来的编译工作需要区分c1还是c2编译器,这个在Java工作线程投递compileTask任务的时候就已经确定(因为在检查阈值的时候即可确定)。在Hotspot中c1编译器用Compiler类作为抽象,c2编译器使用C2Compiler类作为抽象,不管是c1和c2都继承与AbstractCompiler,AbstractCompiler类中给予编译器公共的抽象。为了篇幅的简单,我们采用c1,也叫做client编译器。
/*
invoke_compiler_on_method
->Compiler::compile_method(Compiler为c1编译器)
->Compilation::Compilation
->Compilation::compile_method
*/
// src/share/vm/c1/c1_Compilation.cpp 文件中
void Compilation::compile_method() {
…………
// compile method
int frame_size = compile_java_method();
if (InstallMethods) { // 在c1编译器中,默认为true,也即编译完成后安装机器码。
// 安装代码(实际上就是把机器码执行地址放入Method对象中,下次执行方法就会执行JIT编译好的代码)
install_code(frame_size);
}
…………
}
int Compilation::compile_java_method() {
// 从方法字节码构建出HIR中间语言。
{
PhaseTraceTime timeit(_t_buildIR);
build_hir();
}
// 从HIR中间语言构建出LIR中间语言
{
PhaseTraceTime timeit(_t_emit_lir);
_frame_map = new FrameMap(method(), hir()->number_of_locks(), MAX2(4, hir()->max_stack()));
emit_lir();
}
// 从LIR中间语言生成机器代码
{
PhaseTraceTime timeit(_t_codeemit);
return emit_code_body();
}
}
整个c1编译器的编译过程就论述完毕了(仅仅是宏观上,微观上还存在众多算法和平台指令的生成,这个跟编译原理的算法和Hotspot中JIT优化的思想有关,这里不做论述)
这里对IR中间语言做一个简单的论述,为什么Hotspot中JIT需要存在HIR和LIR 2个中间语言呢?在编译原理中,IR的作用是承上启下,由于中间层的出现,这也让前端和后端更加灵活多变,此时,这也让笔者不经想起一句话" 计算机的世界没有加一个中间层解决不了的问题,如果不行,那就再加一层 ",并且有了中间层的出现,便可以在中间表示层做你想做的语言优化工作。说回Hotspot中,既然分了HIR(High-level Intermediate Representation)和LIR(Low-level Intermediate Representation)两层中间层,所以抽象的行为更加的具体,并且模块化更细粒度。HIR在JIT中是平台无关的中间语言,这一层完全可以用来对接字节码,并且做优化工作,优化完毕后,生成LIR中间语言,而LIR中间语言与平台相关,可以说已经非常接近平台相关指令(机器语言),这一层便可以做平台相关的优化工作。一层一层的过度,这像不像解释器过度到JIT的过程。
在GCC编译器中IR中间层就分为GENERIC、GIMPLE、RTL,也是一层一层的过度,最终RTL最接近机器语言的表现形式
在Coins编译器中,IR中间层也是分为HIR、LIR,跟Hotspot一样,从HIR过度到LIR,LIR接近机器语言的表现形式。