专注虚拟机与编译器研究

第14篇-生成重要的例程

之前介绍过TemplateInterpreter::initialize()函数,在这个函数中初始化了模板表和StubQueue实例,通过如下方式创建InterpreterGenerator实例:

InterpreterGenerator g(_code);

在创建InterpreterGenerator实例时会调用generate_all()函数,如下: 

InterpreterGenerator::InterpreterGenerator(StubQueue* code)
  : TemplateInterpreterGenerator(code) {
   generate_all(); 
}

在generate_all()函数中生成各种例程(机器指令片段)并存储到Interpretercodelet实例中。在HotSpot VM中,不仅有字节码对应的例程,还有许多辅助虚拟机运行时的例程,如之前介绍的普通方法入口entry_point例程,处理异常的例程等等。这些例程都会存储到StubQueue中,如下图所示。 

生成的一些重要例程如下表所示。 

序号 描述 用途
1 error exits 当方法出现时会调用这个例程,进行出错时程序退出
2 bytecode tracing support 配置命令-XX:+TraceBytecodes后,进行字节码追踪
3 return entry points  函数返回入口
4 invoke return entry points

 对于某些invoke字节码调用指令来说,需要一些特殊的

返回入口

5 earlyret entry points  JVMTI的EarlyReturn入口
6 deoptimization entry points  从"逆优化"调用返回的入口 
7 result handlers for native calls 本地方法调用返回值处理handlers 
8 continuation entry points continuation入口 
9 safepoint entry points

safepoint入口,当执行字节码时,如果要求解释执行进入

安全点,则会执行safepoint入口指定的机器指令片段 

10 exception handling 异常处理例程 
11 throw exception entrypoints 抛出异常的入口 
12 all non-native method kinds 非本地方法的入口 
13 all native method kinds 本地方法的入口 
14 Bytecodes  字节码的入口

其中非本地方法的入口、本地方法的入口和字节码的入口比较重要,也是我们后面介绍的重点内容。 这一篇介绍非本地方法的入口和字节码的入口,对于本地方法的入口将在介绍本地方法时详细介绍,这里不过多介绍。

1、非本地方法入口

我们在之前介绍为非本地的普通方法创建Java栈帧的时候提到过,主要的非本地方法入口有如下几类:

enum MethodKind {
    zerolocals,  // 普通的方法             
    zerolocals_synchronized,  // 普通的同步方法         
    ...
}

在generate_all()函数中生成普通方法和普通的同步方法的入口逻辑如下:

{
 CodeletMark cm(_masm, "method entry point (kind = " "zerolocals" ")");
 Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
}
{
 CodeletMark cm(_masm, "method entry point (kind = " "zerolocals_synchronized" ")");
 Interpreter::_entry_table[Interpreter::zerolocals_synchronized] = generate_method_entry(Interpreter::zerolocals_synchronized);
}

调用的generate_method_entry()函数在第6篇已经详细介绍过,最终会生成创建Java栈帧的例程,将例程的首地址存储到Interpreter::_entry_table数组中。

关于同步方法的栈帧建立及特殊逻辑的处理将在介绍锁相关知识时详细介绍,这里不在过多介绍。

除了普通的方法外,还为一些方法生成了一些特殊的入口地址,如为java.lang.Math.sin()、java.lang.Math.cos()等方法生成的例程。如果大家有兴趣可以自己研究一下,这里不在详细介绍。

2、字节码入口

在generate_all()函数中会调用set_entry_points_for_all_bytes()函数,此函数对所有被定义的字节码生成例程并通过对应的属性保存入口,这些入口指向了例程的首地址。set_entry_points_for_all_bytes()函数的实现如下:

void TemplateInterpreterGenerator::set_entry_points_for_all_bytes() {
  for (int i = 0; i < DispatchTable::length; i++) {
     Bytecodes::Code code = (Bytecodes::Code)i;
     if (Bytecodes::is_defined(code)) {
         set_entry_points(code);
     } else {
         set_unimplemented(i);
     }
  }
}

当code是Java虚拟机规范中定义的字节码指令时,调用set_entry_points()函数,此函数取出该字节码指令对应的Template模板并调用set_short_enrty_points()函数进行处理,将入口地址保存在转发表(DispatchTable)_normal_table或_wentry_table(使用wide指令)中。Template模板在之前已经介绍过,字节码指令都会对应一个Template模板,而模板中保存着字节码指令生成对应代码例程中需要的信息。

set_entry_points()函数的实现如下:

void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
  CodeletMark cm(_masm, Bytecodes::name(code), code);

  address bep = _illegal_bytecode_sequence;
  address cep = _illegal_bytecode_sequence;
  address sep = _illegal_bytecode_sequence;
  address aep = _illegal_bytecode_sequence;
  address iep = _illegal_bytecode_sequence;
  address lep = _illegal_bytecode_sequence;
  address fep = _illegal_bytecode_sequence;
  address dep = _illegal_bytecode_sequence;
  address vep = _unimplemented_bytecode;
  address wep = _unimplemented_bytecode;

  // 处理非wide指令,注意指的是那些不能在前面加wide指令的字节码指令
  if (Bytecodes::is_defined(code)) {
     Template* t = TemplateTable::template_for(code);
     set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
  }

  // 处理wide指令,注意指的是那些能在前面加wide指令的字节码指令
  if (Bytecodes::wide_is_defined(code)) {
     Template* t = TemplateTable::template_for_wide(code);
     set_wide_entry_point(t, wep);
  }

  // 当为非wide指令时,共有9个入口,当为wide指令时,只有一个入口
  EntryPoint  entry(bep, cep, sep, aep, iep, lep, fep, dep, vep);
  Interpreter::_normal_table.set_entry(code, entry);
  Interpreter::_wentry_point[code] = wep;
}

注意函数开始时声明时创建了一个变量cm,此时会调用CodeletMark构造函数在StubQueue中创建出存储机器片段的InterpreterCodelet实例,所以调用TemplateInterpreterGenerator::set_short_entry_points()等函数生成的机器指令都会写入到这个实例中。当函数执行完成后,CodeletMark析构函数会提交使用的内存并重置相关属性值。

接下来就是为表示栈顶缓存(Top-of-Stack Caching,缩写为TOSCA,简称Tos)状态的变量赋初始值,其中的_illegal_bytecode_sequence与_unimplemented_bytecode变量指向的也是特定例程的入口地址,这些例程就是在generate_all()函数中生成的,如果大家有兴趣,可以研究一下这些例程是怎么处理非法字节码等情况的。

调用set_short_entry_points()函数时,需要传入栈顶缓存状态,也就是上一个字节码执行时可能会将产生的结果存储到寄存器中。使用栈顶缓存主要还是为了提高解释执行的效率。HotSpot VM共定义了9种TosState,通过枚举常量来表示,如下:

enum TosState {      // describes the tos cache contents
  btos = 0,          // byte, bool tos cached
  ctos = 1,          // char tos cached
  stos = 2,          // short tos cached
  itos = 3,          // int tos cached
  ltos = 4,          // long tos cached
  ftos = 5,          // float tos cached
  dtos = 6,          // double tos cached
  atos = 7,          // object cached
  vtos = 8,          // tos not cached
  number_of_states,
  ilgl               // illegal state: should not occur
};

以非wide指令为例进行说明,bep(byte entry point)、cep、 sep、aep、iep、lep、fep、dep、vep分别表示指令执行前栈顶元素状态为byte/boolean、char、short、array/reference(对象引用)、int、long、float、double、void类型时的入口地址。举个例子,如iconst_0表示向栈中压入常量0,那么字节码指令模板中有如下定义:

def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst,0);

第3个参数指明了tos_in,第4个参数为tos_out,tos_in与tos_out是指令执行前后的TosState。也就是说,执行此字节码指令之前不需要获取栈顶缓存的值,所以为void;执行完成后栈顶会缓存一个int类型的整数,也就是0。缓存通常会缓存到寄存器中,所以比起压入栈中,获取的效率要更高一些,如果下一个执行的字节码指令不需要,那么还需要将缓存的0值压入栈内。假设下一个执行的字节码也为iconst,那么要从iconst指令的iep(上一个缓存了int类型整数0)入口来执行,由于iconst的入口要求为vtos,所以需要将寄存器中的int类型数值0入栈。所以每个字节码指令都会有多个入口,这样任何一个字节码指令在执行完成后,都可以根据当前执行后的栈顶缓存状态找到下一个需要执行字节码的对应入口。

再回头看一下我们第8篇介绍的分发字节码相关内容,为各个字节码设置入口的函数DispatchTable::set_entry(),其中的_table的一维为栈顶缓存状态,二维为Opcode,通过这2个维度能够找到一段机器指令,这就是根据当前的栈顶缓存状态定位到的字节码需要执行的例程。我们看一下TemplateInterpreterGenerator::set_entry_points()函数,最后会调用DispatchTable::set_entry()函数为_table属性赋值。这样类型为DispatchTable的TemplateInterpreter::_normal_table与TemplateInterpreter::_wentry_point变量就可以完成字节码分发了。

调用TemplateTable::template_for()函数可以从TemplateTable::_template_table数组中获取对应的Template实例,然后调用set_short_entry_points()函数生成例程。非wild指令调用set_short_entry_points()函数,set_short_entry_points()函数的实现如下: 

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
) {
  switch (t->tos_in()) {
    case btos:
    case ctos:
    case stos:
      ShouldNotReachHere();  
      break;
    case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t);   break;
    case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t);   break;
    case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t);   break;
    case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t);   break;
    case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t);   break;
    case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);  break;
    default  : ShouldNotReachHere();                                                   break;
  }
}

set_short_entry_points()函数会根据Template实例中保存的字节码模板信息生成最多9个栈顶入口并赋值给传入参数bep、cep等,也就是给Template代表的特定字节码指令生成相应的入口地址。

set_short_entry_points()函数根据操作数栈栈顶元素类型进行判断,首先byte、char和short类型都应被当做int类型进行处理,所以不会为字节码指令生成这几个类型的入口地址;如果当前字节码执行之前要求有栈顶元素并且类型是atos对象类型,那么当没有栈顶缓存时,从vep入口进入,然后弹出表达式栈中的对象到栈顶缓存寄存器后,就可以直接从aep进入,itos、ltos、ftos和dtos也都类似,会分别成为2个入口地址;如果不要求有栈顶元素,那么就是vtos,非void类型将调用generate_and_dispatch()函数生成各种入口。

set_vtos_entry_points()函数的实现如下:

void TemplateInterpreterGenerator::set_vtos_entry_points(
 Template* t,
 address& bep,
 address& cep,
 address& sep,
 address& aep,
 address& iep,
 address& lep,
 address& fep,
 address& dep,
 address& vep) {
  Label L;
  aep = __ pc();  __ push_ptr();  __ jmp(L);
  fep = __ pc();  __ push_f();    __ jmp(L);
  dep = __ pc();  __ push_d();    __ jmp(L);
  lep = __ pc();  __ push_l();    __ jmp(L);
  bep = cep = sep =
  iep = __ pc();  __ push_i();
  vep = __ pc();
  __ bind(L);
  generate_and_dispatch(t);
}

如果字节码不要求有栈顶缓存时(即vtos状态),会为当前字节码生成9个入口地址,由bep、cep等保存下来。如生成aep入口时,因为当前执行的字节码栈不需要顶缓存状态,所以要把值压入表达式栈中,然后跳转到L处执行,也就是相当于从vep入口进入执行了。

现在简单梳理一下,上一个字节码指令到底从哪个入口进入到下一个字节码指令要通过上一个字节码指令的执行结果而定。如果上一个字节码指令执行的结果为fep,而当前字节码指令执行之前的栈顶缓存状态要求是vtos,则从TemplateInterpreterGenerator::set_vtos_entry_points()函数中给fep赋值的地方开始执行。所以说,上一个字节码指令的执行结果和下一个将要执行的字节码指令执行之前要求的栈顶缓存状态共同决定了从哪个入口进入。

push_f()函数的实现如下:

源代码位置:/hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp
void InterpreterMacroAssembler::push_f(XMMRegister r) { // r的默认值为xmm0
    subptr(rsp, wordSize);       // wordSize为机器字长,64位下为8字节,所以值为8
    movflt(Address(rsp, 0), r);
}

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

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

void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {
  assert(isByte(op1) && isByte(op2), "wrong opcode");
  assert((op1 & 0x01) == 1, "should be 32bit operation");
  assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
  if (is8bit(imm32)) {
    emit_int8(op1 | 0x02); // set sign bit
    emit_int8(op2 | encode(dst));
    emit_int8(imm32 & 0xFF);
  } else {
    emit_int8(op1);
    emit_int8(op2 | encode(dst));
    emit_int32(imm32);
  }
}

调用emit_arith()、emit_int8()等函数生成机器指令片段,生成的内容最后会存储到StubQueue的InterpreterCodelet实例中,关于机器指令和生成存储过程在之前已经介绍过,这里不做过多介绍。

set_vtos_entry_points()函数生成的机器指令片段经过反编译后,对应的汇编代码后如下:

// aep的入口
push   %rax           
jmpq   L 

// fep入口
sub    $0x8,%rsp      
movss  %xmm0,(%rsp)
jmpq   L
       
// dep入口  
sub    $0x10,%rsp     
movsd  %xmm0,(%rsp)
jmpq   L

// lep入口
sub    $0x10,%rsp    
mov    %rax,(%rsp)
jmpq   L

// iep入口
push   %rax   

// ---- L ----     

set_vtos_entry_points()函数最后调用generate_and_dispatch()函数写入当前字节码指令对应的机器指令片段和跳转到下一个字节码指令继续执行的逻辑处理部分。

generate_and_dispatch()函数的主要实现如下:

void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
  // 生成当前字节码指令对应的机器指令片段
  t->generate(_masm);

  if (t->does_dispatch()) {
     // asserts
  } else {
     // 生成分发到下一个字节码指令的逻辑
     __ dispatch_epilog(tos_out, step);
  }
}

这里以iconst字节码为例分析generate()函数的实现:

void Template::generate(InterpreterMacroAssembler* masm) {
  // parameter passing
  TemplateTable::_desc = this;
  TemplateTable::_masm = masm;
  // code generation
  _gen(_arg);
  masm->flush();
}

generate()函数会调用生成器函数_gen(_arg),对于iconst指令来说,生成器函数为iconst()。generate()函数根据平台而不同,如x86_64平台下,定义如下:

源代码位置:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::iconst(int value) {
  if (value == 0) {
    __ xorl(rax, rax);
  } else {
    __ movl(rax, value);
  }
}

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

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

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

dispatch_next()函数的实现如下:

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

这个函数在之前已经介绍过,这里不再介绍。

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

第6篇-Java方法新栈帧的创建

第7篇-为Java方法创建栈帧

第8篇-dispatch_next()函数分派字节码

第9篇-字节码指令的定义

第10篇-初始化模板表

第11篇-认识Stub与StubQueue

第12篇-认识CodeletMark

第13篇-通过InterpreterCodelet存储机器指令片段

第14篇-生成重要的例程

如果有问题可直接评论留言或加作者微信mazhimazh

关注公众号,有HotSpot VM源码剖析系列文章!

 

  

 

 

 

 

 

 

 

 

  

  

posted on 2021-08-31 10:19  鸠摩(马智)  阅读(137)  评论(0编辑  收藏  举报

导航