首页 > HotSpot模板解释器目标代码生成过程源码分析

HotSpot模板解释器目标代码生成过程源码分析

  虽然说解释执行模式是逐字逐句翻译给目标平台运行的,但这样的过程未免太过缓慢,如果能把字节码说的话做成纸条,运行时只要把对应的纸条交给目标平台就可以了,这样,执行速度就会明显提升。JVM的Hotspot虚拟机的模板解释器就是用这种方法来解释执行的。在开始分析之前,先了解一下JVM的执行方式。

  (1).边解释边运行,即每次解释一条字节码并运行其解释的本地代码,这种执行引擎速度相对很慢 

  (2).JIT(即时编译)具有更快的运行速度但是需要更多的内存,方法被第一次调用时,字节码编译生成的本地代码将会被缓存,这样在该方法下次被调用的时候,将取出缓冲的本地代码直接运行 

  (3).自适应优化,对于经常被调用的方法,则会缓存其编译产生成的本地代码,对于其他较少被调用的代码,仍对其采用解释执行的方法。 

  (4).片上虚拟机,即虚拟机的执行引擎直接嵌入在片上

  HotSpot虚拟机可以配置为以下运行模式: 

    -Xint:解释模式 

    -Xcomp:编译模式 

    -Xmixed:混合模式 

(通过java -version就可以查看虚拟机的运行模式)

  HotSpot在启动时,会为所有字节码创建在目标平台上运行的解释运行的机器码,并存放在CodeCache中,在解释执行字节码的过程中,就会从CodeCache中取出这些本地机器码并执行。

  Hotspot虚拟机的细节技术实现值得借鉴,如果你觉得源码甚至汇编代码比较枯燥的话,也可以大致了解相关模块的组件、工作流程,对相关实现有一定的认识。

  下面就从模板解释器的初始化开始,分析HotSpot的解释代码的生成。

  在创建虚拟机时,在初始化全局模块过程中,会调用interpreter_init()初始化模板解释器,模板解释器的初始化包括抽象解释器AbstractInterpreter的初始化、模板表TemplateTable的初始化、CodeCache的Stub队列StubQueue的初始化、解释器生成器InterpreterGenerator的初始化

 1 void TemplateInterpreter::initialize() {
 2   if (_code != NULL) return;
 3   // assertions
 4   //...
 5 
 6   AbstractInterpreter::initialize();
 7 
 8   TemplateTable::initialize();
 9 
10   // generate interpreter
11   { ResourceMark rm;
12     TraceTime timer("Interpreter generation", TraceStartupTime);
13     int code_size = InterpreterCodeSize;
14     NOT_PRODUCT(code_size *= 4;)  // debug uses extra interpreter code space
15     _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,
16                           "Interpreter");
17     InterpreterGenerator g(_code);
18     if (PrintInterpreter) print();
19   }
20 
21   // initialize dispatch table
22   _active_table = _normal_table;
23 }

1.AbstractInterpreter是基于汇编模型的解释器的共同基类,定义了解释器和解释器生成器的抽象接口。 

2.模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数)。 

TemplateTable的初始化调用def()将所有字节码的目标代码生成函数和参数保存在_template_table或_template_table_wide(wide指令)模板数组中

  //                              interpr. templates// Java spec bytecodes          ubcp|disp|clvm|iswd  in    out   generator      argumentdef(Bytecodes::_nop           , ____|____|____|____, vtos, vtos, nop           ,  _      );def(Bytecodes::_aconst_null   , ____|____|____|____, vtos, atos, aconst_null   ,  _      );def(Bytecodes::_iconst_m1     , ____|____|____|____, vtos, itos, iconst        , -1      );def(Bytecodes::_iconst_0      , ____|____|____|____, vtos, itos, iconst        ,  0      );def(Bytecodes::_iconst_1      , ____|____|____|____, vtos, itos, iconst        ,  1      );def(Bytecodes::_iconst_2      , ____|____|____|____, vtos, itos, iconst        ,  2      );
//...其他字节码的模板定义

其中,def()是查看数组对应项是否为空,若为空则初始化该数组项。

Template* t = is_wide ? template_for_wide(code) : template_for(code);// setup entryt->initialize(flags, in, out, gen, arg);

_template_table或_template_table_wide的数组项就是Template对象,即字节码的模板,Template的结构如下:

class Template VALUE_OBJ_CLASS_SPEC {private:enum Flags {uses_bcp_bit,                                // set if template needs the bcp pointing to bytecodedoes_dispatch_bit,                           // set if template dispatches on its owncalls_vm_bit,                                // set if template calls the vmwide_bit                                     // set if template belongs to a wide instruction
  };typedef void (*generator)(int arg);int       _flags;                  // describes interpreter template properties (bcp unknown)TosState  _tos_in;                 // tos cache state before template executionTosState  _tos_out;                // tos cache state after  template executiongenerator _gen;                    // template code generatorint       _arg;   

_flags为标志,该项的低四位分别标志:

  • uses_bcp_bit,标志需要使用字节码指针(byte code pointer,数值为字节码基址+字节码偏移量)
  • does_dispatch_bit,标志是否在模板范围内进行转发,如跳转类指令会设置该位
  • calls_vm_bit,标志是否需要调用JVM函数
  • wide_bit,标志是否是wide指令(使用附加字节扩展全局变量索引)

_tos_in表示模板执行前的TosState(操作数栈栈顶元素的数据类型,TopOfStack,用来检查模板所声明的输出输入类型是否和该函数一致,以确保栈顶元素被正确使用) 

 _tos_out表示模板执行后的TosState 

 _gen表示模板生成器(函数指针) 

 _arg表示模板生成器参数

3.StubQueue是用来保存生成的本地代码的Stub队列,队列每一个元素对应一个InterpreterCodelet对象,InterpreterCodelet对象继承自抽象基类Stub,包含了字节码对应的本地代码以及一些调试和输出信息。 

其内存结构如下:在对齐至CodeEntryAlignment后,紧接着InterpreterCodelet的就是生成的目标代码。 

      

4.InterpreterGenerator根据虚拟机使用的解释器模型不同分为别CppInterpreterGenerator和TemplateInterpreterGenerator 

根据不同平台的实现,以x86_64平台为例,TemplateInterpreterGenerator定义在/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp

1 InterpreterGenerator::InterpreterGenerator(StubQueue* code)
2   : TemplateInterpreterGenerator(code) {
3    generate_all(); // down here so it can be "virtual"
4 }

(1).在TemplateInterpreterGenerator的generate_all()中,将生成一系列JVM运行过程中所执行的一些公共代码和所有字节码的InterpreterCodelet

  • error exits:出错退出处理入口
  • 字节码追踪入口(配置了-XX:+TraceBytecodes)
  • 函数返回入口
  • JVMTI的EarlyReturn入口
  • 逆优化调用返回入口
  • native调用返回值处理handlers入口
  • continuation入口
  • safepoint入口
  • 异常处理入口
  • 抛出异常入口
  • 方法入口(native方法和非native方法)
  • 字节码入口

(2).其中,set_entry_points_for_all_bytes()会对所有被定义的字节码生成目标代码并设置对应的入口(这里只考虑is_defined的情况)

 1 void TemplateInterpreterGenerator::set_entry_points_for_all_bytes() {
 2   for (int i = 0; i < DispatchTable::length; i++) {
 3     Bytecodes::Code code = (Bytecodes::Code)i;
 4     if (Bytecodes::is_defined(code)) {
 5       set_entry_points(code);
 6     } else {
 7       //未被实现的字节码(操作码)
 8       set_unimplemented(i);
 9     }
10   }
11 }

(3).set_entry_points()将取出该字节码对应的Template模板,并调用set_short_enrty_points()进行处理,并将入口地址保存在转发表(DispatchTable)_normal_table或_wentry_table(使用wide指令)中

 1 void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
 2   CodeletMark cm(_masm, Bytecodes::name(code), code);
 3   // initialize entry points
 4   // ... asserts
 5   address bep = _illegal_bytecode_sequence;
 6   address cep = _illegal_bytecode_sequence;
 7   address sep = _illegal_bytecode_sequence;
 8   address aep = _illegal_bytecode_sequence;
 9   address iep = _illegal_bytecode_sequence;
10   address lep = _illegal_bytecode_sequence;
11   address fep = _illegal_bytecode_sequence;
12   address dep = _illegal_bytecode_sequence;
13   address vep = _unimplemented_bytecode;
14   address wep = _unimplemented_bytecode;
15   // code for short & wide version of bytecode
16   if (Bytecodes::is_defined(code)) {
17     Template* t = TemplateTable::template_for(code);
18     assert(t->is_valid(), "just checking");
19     set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
20   }
21   if (Bytecodes::wide_is_defined(code)) {
22     Template* t = TemplateTable::template_for_wide(code);
23     assert(t->is_valid(), "just checking");
24     set_wide_entry_point(t, wep);
25   }
26   // set entry points
27   EntryPoint entry(bep, cep, sep, aep, iep, lep, fep, dep, vep);
28   Interpreter::_normal_table.set_entry(code, entry);
29   Interpreter::_wentry_point[code] = wep;
30 }

这里以非wide指令为例分析set_short_entry_points()。bep(byte entry point), cep, sep, aep, iep, lep, fep, dep, vep分别为指令执行前栈顶元素状态为byte/boolean、char、short、array/reference(对象引用)、int、long、float、double、void类型时的入口地址。

(4).set_short_entry_points()根据操作数栈栈顶元素类型进行判断,首先byte类型、char类型和short类型都应被当做int类型进行处理,对于非void类型将调用generate_and_dispatch()产生目标代码,这里以iconst_0为例对TOS的处理进行介绍: 

对于iconst,其期望的_tos_in(执行前栈顶元素类型)是void类型(vtos),期望的_tos_out(执行后栈顶元素类型)是int类型(itos)

 1 void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) {
 2   assert(t->is_valid(), "template must exist");
 3   switch (t->tos_in()) {
 4     case btos:
 5     case ctos:
 6     case stos:
 7       ShouldNotReachHere();  // btos/ctos/stos should use itos.
 8       break;
 9     case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t); break;
10     case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break;
11     case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t); break;
12     case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t); break;
13     case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t); break;
14     case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);     break;
15     default  : ShouldNotReachHere();                                                 break;
16   }
17 }

其中__定义如下:

# define __ _masm->

即模板解释器的宏汇编器

(5).以期望的栈顶状态为vtos状态为例,分析set_vtos_entry_points()

 1 void TemplateInterpreterGenerator::set_vtos_entry_points(Template* t,
 2                                                          address& bep,
 3                                                          address& cep,
 4                                                          address& sep,
 5                                                          address& aep,
 6                                                          address& iep,
 7                                                          address& lep,
 8                                                          address& fep,
 9                                                          address& dep,
10                                                          address& vep) {
11   assert(t->is_valid() && t->tos_in() == vtos, "illegal template");
12   Label L;
13   aep = __ pc();  __ push_ptr();  __ jmp(L);
14   fep = __ pc();  __ push_f();    __ jmp(L);
15   dep = __ pc();  __ push_d();    __ jmp(L);
16   lep = __ pc();  __ push_l();    __ jmp(L);
17   bep = cep = sep =
18   iep = __ pc();  __ push_i();
19   vep = __ pc();
20   __ bind(L);
21   generate_and_dispatch(t);
22 }

以ftos入口类型为例(vtos即当前字节码的实现不关心栈顶元素的状态),分析该入口的处理指令: 

push_f(): 

  定义在 /hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp中

1 void InterpreterMacroAssembler::push_f(XMMRegister r) {
2   subptr(rsp, wordSize);
3   movflt(Address(rsp, 0), r);
4 }

  其中r的默认值为xmm0,wordSize为机器字长(如64位机器为8字节)

subptr()实际上调用了subq():

1 void MacroAssembler::subptr(Register dst, int32_t imm32) {
2   LP64_ONLY(subq(dst, imm32)) NOT_LP64(subl(dst, imm32));
3 }

subq()的实现如下:

1 void Assembler::subq(Register dst, int32_t imm32) {
2   (void) prefixq_and_encode(dst->encoding());
3   emit_arith(0x81, 0xE8, dst, imm32);
4 }

而emit_arith()将调用emit_byte()/emit_long()写入指令的二进制代码”83 EC 08”(由于8可由8位有符号数表示,第一个字节为0x81 | 0x02,即0x83,rsp的寄存器号为4,第二个字节为0xE8 | 0x04,即0xEC,第三个字节为0x08 & 0xFF,即0x08),该指令即AT&T风格的sub $0x8,%rsp

 1 void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {
 2   assert(isByte(op1) && isByte(op2), "wrong opcode");
 3   assert((op1 & 0x01) == 1, "should be 32bit operation");
 4   assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
 5   if (is8bit(imm32)) { //iconst_0的操作数为0,即可以用8位二进制数表示
 6     emit_byte(op1 | 0x02); // set sign bit
 7     emit_byte(op2 | encode(dst));
 8     emit_byte(imm32 & 0xFF);
 9   } else {
10     emit_byte(op1);
11     emit_byte(op2 | encode(dst));
12     emit_long(imm32);
13   }
14 }

emit_byte()定义在/hotspot/src/share/vm/asm/assembler.inlilne.hpp中: 

该函数将把该字节复制到_code_pos处

1 inline void AbstractAssembler::emit_byte(int x) {
2   assert(isByte(x), "not a byte");
3   *(unsigned char*)_code_pos = (unsigned char)x;
4   _code_pos += sizeof(unsigned char);
5   sync();
6 }

故subq()向代码缓冲写入了指令sub $0x8,%rsp 

类似地,movflt()向代码缓冲写入了指令 movss %xmm0,(%rsp) 

jmp()向代码缓冲写入了指令jmpq (addr为字节码的本地代码入口) 

set_vtos_entry_points()产生的入口部分代码如下:

 1 push %rax        .....(atos entry)
 2 jmpq  
 3 sub $0x8,%rsp     .....(ftos entry)
 4 movss %xmm0,(%rsp)
 5 jmpq (addr为字节码的本地代码入口)
 6 sub $0x10,%rsp    .....(dtos entry)
 7 movsd %xmm0,(%rsp)
 8 jmpq 
 9 sub $0x10,%rsp     .....(ltos entry)
10 mov %rax,(%rsp)
11 jmpq 
12 push %rax         ...(itos entry)

set_vtos_entry_points()的最后调用generate_and_dispatch()写入了当前字节码的解释代码和跳转到下一个字节码继续执行的逻辑处理部分

generate_and_dispatch()主要内容如下:

 1 void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
 2   // ...
 3   // generate template
 4   t->generate(_masm);
 5   // advance
 6   if (t->does_dispatch()) {
 7     //asserts
 8   } else {
 9     // dispatch to next bytecode
10     __ dispatch_epilog(tos_out, step);
11   }
12 }

这里我们以iconst()为目标代码生成器为例,分析generate():

1 void Template::generate(InterpreterMacroAssembler* masm) {
2   // parameter passing
3   TemplateTable::_desc = this;
4   TemplateTable::_masm = masm;
5   // code generation
6   _gen(_arg);
7   masm->flush();
8 }

generate()会调用生成器函数_gen(_arg),该函数根据平台而不同,如x86_64平台下,定义在/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp中

1 void TemplateTable::iconst(int value) {
2   transition(vtos, itos);
3   if (value == 0) {
4     __ xorl(rax, rax);
5   } else {
6     __ movl(rax, value);
7   }
8 }

我们知道,iconst_i指令是将i压入栈,这里生成器函数iconst()在i为0时,没有直接将0写入rax,而是使用异或运算清零,即向代码缓冲区写入指令”xor %rax, %rax”;在i不为0时,写入指令”mov $0xi, %rax” 

当不需要转发时,会调用dispatch_epilog()生成取下一条指令和分派的目标代码:

1 void InterpreterMacroAssembler::dispatch_epilog(TosState state, int step) {
2   dispatch_next(state, step);
3 }

dispatch_next()实现如下:

1 void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {
2   // load next bytecode (load before advancing r13 to prevent AGI)
3   load_unsigned_byte(rbx, Address(r13, step));
4   // advance r13
5   increment(r13, step);
6   dispatch_base(state, Interpreter::dispatch_table(state));
7 }

dispatch_next()首先调用load_unsigned_byte()写入指令”movzbl (%r13),%rbx”,再调用increment()写入指令”inc/add (,)%r13”指令,最后调用dispatch_base()写入”jmp *(%r10,%rbx,8)”。这类似于PC自增一条指令的宽度再继续取值运行的过程。

 

分析到这里,不禁有一个疑问,_code_pos是哪里?之前说过,StubQueue是用来保存生成的本地代码的Stub队列,队列每一个元素对应一个InterpreterCodelet对象,InterpreterCodelet对象包含了字节码对应的本地代码以及一些调试和输出信息。那么_code_pos是如何和InterpreterCodelet对应的呢? 

我们注意到无论是为JVM的各种入口函数,还是为字节码生成本地代码,都会构造一个CodeletMark对象

CodeletMark cm(_masm, Bytecodes::name(code), code);

CodeletMark的构造函数如下:在初始值列表中,调用了StubQueue的request()创建了一个InterpreterCodelet对象,并以该InterpreterCodelet目标代码地址和大小为参数构造了一块CodeBuffer用来存放生成的目标代码。

public:CodeletMark(InterpreterMacroAssembler*& masm,const char* description,Bytecodes::Code bytecode = Bytecodes::_illegal):_clet((InterpreterCodelet*)AbstractInterpreter::code()->request(codelet_size())),_cb(_clet->code_begin(), _clet->code_size()){ // request all space (add some slack for Codelet data)assert (_clet != NULL, "we checked not enough space already");// initialize Codelet attributes_clet->initialize(description, bytecode);// create assembler for code generationmasm  = new InterpreterMacroAssembler(&_cb);_masm = &masm;}

但在此时还未生成目标代码,所以并不知道生成的目标代码有多大,所以这里会向StubQueue申请全部的空闲空间(只留有一点用来对齐空间,注意StubQueue实际上是一片连续的内存空间,所有Stub都在该空间上进行分配) 

随后初始化该InterpreterCodelet的描述部分和对应字节码,并以该CodeBuffer为参数构造了一个编译器对象InterpreterMacroAssembler

分析到这里,就应该明白编译器的_code_pos指的就是生成代码在CodeBuffer中的当前写位值 

还需一提的就是CodeletMark的析构函数,这里确认编译器的生产代码完全写入到CodeBuffer中后,就会调用StubQueue的commit()将占用的空间划分为当前Stub(InterpreterCodelet)所有

~CodeletMark() {// align so printing shows nop's instead of random code at the end (Codelets are aligned)(*_masm)->align(wordSize);// make sure all code is in code buffer(*_masm)->flush();// commit CodeletAbstractInterpreter::code()->commit((*_masm)->code()->pure_insts_size());// make sure nobody can use _masm outside a CodeletMark lifespan*_masm = NULL;}

 

转载于:https://www.cnblogs.com/iceAeterNa/p/4876924.html

更多相关:

  • 『转载』Debussy快速上手(Verdi相似) Debussy 是NOVAS Software, Inc(思源科技)发展的HDL Debug & Analysis tool,这套软体主要不是用来跑模拟或看波形,它最强大的功能是:能够在HDL source code、schematic diagram、waveform、state b...

  • THE START更新堪称轻量级MATLAB的一款软件最新版-Maplesoft Maple 2019.2 中文版。Maple是符号和数字计算环境,也是一种多范式编程语言,由Maplesoft开发,还涵盖了技术计算的其他方面,包括可视化,数据分析,矩阵计算和MATLAB连接。MapleSim工具箱添加了用于多域物理建模和代码生成的...

  • 同学们,你们在学习他人的代码,是否见过这样的代码 def main(): def user_info(gender): 当你还是个小萌新时,你一定会认为这是个很牛逼的语法。 当你有了一点基础时,你一定会想要了解这个语法,并且尝试去使用它。 那么今天,我们便来了解这个牛语法。 有了一点点的python基础,我们来看这段代...

  •     自从用了这些快捷键,鼓励师也不需要了,代码开发效率蹭蹭提升!!! ctrl+shift+[折叠代码 (这个比ctrl+k ctrl+l、ctrl+k ctr+j不知道好用多少倍!) ctrl+shift+]展开代码 ctrl+shift+T打开手贱不小心关掉的窗口 【推荐】ctrl+shift+O打开当前文件...

  • 在提交代码之前,建议最好先Fetch代码下来(如果有冲突,系统会提示),然后再操作Merge到本地分支,这样做是为了避免有其他人同时修改了当前分支,如果直接用Ctrl+T(pull代码)极有可能覆盖本地分支最新代码,安全起见先Fetch代码(Ctrl+Alt+Shift+1)——所谓:小心驶得万年船!...

  • 每次复制代码时,如果代码里有 // 这样的注释就容易让格式乱掉,通过下面的设置就可以避免这种情况。 粘贴代码时取消自动缩进 VIM在粘贴代码时会自动缩进,把代码搞得一团糟糕,甚至可能因为某行的一个注释造成后面的代码全部被注释掉,我知道有同学这个时候会用vi去打开文件再粘贴上去(鄙人以前就是这样),其实需要先设置一下 s...

  • 字节串bytes字节串也叫字节序列,是不可变的序列,存储以字节为单位的数据字节串表示方法:b"ABCD"b"x41x42"...字节串的构造函数:bytes() 创建一个空的字节串 ,同b””bytes(整数可迭代对象) 用可迭代对象创建一个字节串bytes(整数n) 生成n个值为0的字节串bytes(字符串,encoding='...

  • Unicode编码  最初的unicode编码是固定长度的,16位,也就是2两个字节代表一个字符,这样一共可以表示65536个字符。显然,这样要表示各种语言中所有的字符是远远不够的。Unicode4.0规范考虑到了这种情况,定义了一组附加字符编码,附加字符编码采用2个16位来表示,这样最多可以定义1048576个附加字符。所以4个字节...

  • Java IO流学习总结三:缓冲流-BufferedInputStream、BufferedOutputStream 转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/54894451 本文出自【赵彦军的博客】 InputStream |__FilterInputSt...

  •   一直对编码这块晕晕乎乎,今天终于看到一篇写的很清楚也很风趣的文章,转过来mark一下。 很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为”字节“。再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始...

  • 我们知道在由于大端机和小端机导致网络字节序和主机序有可能是有差异的,我们可以使用系统的ntohs,ntohl,htons和htonl这些处理函数进行转换,下面是我写的一个关于ntohs在处理小端机字节序转换的函数的简单实现. 思想大致如下: 用u_int16_t的2字节16位的整形变量来存储这个整数,首先将第一个字节和该变量进行或运算...