专注虚拟机与编译器研究

第62篇-解释器与编译器适配(一)

对栈上替换的nmethod而言,执行栈上替换就相当于安装,因为栈上替换的nmethod都是方法内部的调用,所以实现相对简单点。对非栈上替换的nmethod而言,其安装稍微复杂点,需要考虑从Java代码和本地代码中调用nmethod安装完成的方法的情形,HotSpot VM的实现是通过一个在字节码解释执行的栈帧和本地代码执行的栈帧之间做切换适配的适配器来完成安装,适配器和字节码指令一样都是通过汇编实现的。

下面看一下解释执行与编译执行的入口,调用的函数link_method()的实现如下:

void Method::link_method(methodHandle h_method, TRAPS) {
  if (_i2i_entry != NULL){
	  return;
  }
  // 设置_i2i_entry和_from_interpreted_entry属性的值
  address entry = Interpreter::entry_for_method(h_method);
  set_interpreter_entry(entry);

  // 设置Method::_from_compiler_entry和Method::_adapter属性的值
  (void) make_adapters(h_method, CHECK);
}

方法链接主要就是做的事就是设置 Method::_from_interpreter_entry,过程主要是根据方法类型,获取并保存方法对应的入口例程的地址。  

《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中详细介绍了方法Method及其中定义的重要属性,如_i2i_entry 与 _from_interpreted_entry等,这里不再介绍。set_interpreter_entry()函数设置了_i2i_entry 与 _from_interpreted_entry这2个属性的值,如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/method.hpp文件

void set_interpreter_entry(address entry){
 _i2i_entry = entry;
 _from_interpreted_entry = entry;
}

_i2i_entry指向方法的解释器入口,此值设置后不会再改变。在方法连接时,_from_interpreted_entry和_i2i_entry指向的都是之前详细介绍过的、调用generate_call_stub()函数生成的解释执行的入口例程。

_from_interpreted_entry 初始的值与 _i2i_entry 一样,但后面当该Java方法被JIT编译并“安装”之后,_from_interpreted_entry 就会被设置为指向 i2c adapter stub(设置逻辑在前一篇介绍的Method::set_code()函数中)。而如果因为某些原因需要抛弃掉之前已经编译并安装好的机器码,则 _from_interpreted_entry 会被恢复为 _i2i_entry(恢复逻辑在前一篇介绍的Method::clear_code()函数中)。如下图所示。

  

如上的Method::link_method()函数中调用的make_adapters()函数的实现如下:

address Method::make_adapters(methodHandle mh, TRAPS) {
  AdapterHandlerEntry* adapter = AdapterHandlerLibrary::get_adapter(mh);

  mh->set_adapter_entry(adapter);
  mh->_from_compiled_entry = adapter->get_c2i_entry();
  return adapter->get_c2i_entry();
}

Method::_adapter属性就是一个AdapterHandlerEntry指针,AdapterHandlerEntry表示一个栈帧转换的适配器,因为字节码解释执行时的栈帧结构和寄存器的使用与编译后的本地代码执行时的完全不同,需要在字节码解释执行和本地代码执行两者之间切换时对栈帧和寄存器等做必要的转换处理。允许从字节码解释执行切换到本地代码执行(即I2C)以及从本地代码执行切换到字节码解释执行(即C2I)。AdapterHandlerEntry本身很简单,只是一个保存I2C和C2I适配器地址的容器而已,此类及重要属性的定义如下:

class AdapterHandlerEntry : public BasicHashtableEntry<mtCode> {
 private:
  // 方法签名相同时,可以重用适配器例程,所以_fingerprint代表着某个方法的签名
  AdapterFingerPrint* _fingerprint;
  // 解释执行切换到编译执行的适配器,通过调用_adapter->i2c_entry()函数获取
  address  _i2c_entry; 
  // 编译执行切换到解释执行的适配器,通过调用_adapter->c2i_entry()函数获取
  address  _c2i_entry; 
  // 编译执行切换到解释执行的适配器,通过调用_adapter->get_c2i_unverified_entry()函数获取
  address  _c2i_unverified_entry; 
  // ...
}

 AdapterHandlerLibrary是一个用来生成AdapterHandlerEntry的一个工具类,其中定义的get_apapter()函数非常重要,会创建AdapterHandlerEntry实例并初始化其中的_i2c_entry、_c2i_entry等属性。调用get_c2i_entry()函数获取_c2i_entry属性的值并初始化_from_compiled_entry属性,如下图所示。

从adapter中调用get_i2c_entry()或get_c2i_entry()函数就可以在解释执行和编译执行之间进行适配,因为解释执行和编译执行的调用约定(calling convention)不同,所以要进行适配。

_from_compiled_entry被初始化为指向c2i adapter stub(方法连接时调用Method::make_adapters()函数设置)),因为方法在开始的时候并没有被JIT编译,只能解释执行。如果从已编译的Java方法调用过来的话就需要适配调用约定。当方法被JIT编译并“安装”完之后,_from_compiled_entry会指向编译出来的机器码的入口,具体说就是指向verified entry point(设置逻辑在前一篇介绍的Method::set_code()函数中)。如果要抛弃之前编译好的机器码,那么 _from_compiled_entry 会恢复指向为c2i adapter stub(恢复逻辑在前一篇介绍的Method::clear_code()函数中)。

当编译完成后,会对编译完成的方法生成一个nmethod实例。nmethod类的全名native method,指向的是Java方法编译的一个版本。通过Method::_code属性来存储,如下:

nmethod* volatile  _code;

Method::_code指向JIT编译后的机器码。初始值为NULL,意味着该方法尚未被JIT编译。当一个方法被JIT编译并“安装”后,_code 就会指向编译生成的 nmethod,而要抛弃编译好的代码时,_code属性重新设置为NULL即可。实际上,在nmethod类中也定义了2个编译方法的入口,由如下2个属性保存:

// 需要进行类检测的入口
address           _entry_point;           
// 没有类检查的入口
address           _verified_entry_point;  

每个Method有两个实际入口,一个是unverified entry point(UEP),用于实现虚方法分派的monomorphic inline cache;另一个是verified entry point(VEP),是方法的真正入口。只有需要虚方法分派的方法才会有独立的UEP;对静态方法、私有成员方法之类的Java方法,UEP与VEP实际上在同一个位置,后面还会详细介绍UEP与VEP 。 

在Method::make_adapters()函数中调用AdapterHandlerLibrary::get_adapter()函数获取AdapterHandlerEntry,这样就能获取到i2c、c2i等stub的入口地址了。下面看一下get_adapter()函数的实现,如下:

// 传递了method参数,所以是针对特定方法来实现的
AdapterHandlerEntry* AdapterHandlerLibrary::get_adapter(methodHandle method) {
  address ic_miss = SharedRuntime::get_ic_miss_stub();

  ResourceMark rm;

  AdapterBlob*          B = NULL;
  AdapterHandlerEntry*  entry = NULL;
  AdapterFingerPrint*   fingerprint = NULL;

  ///////////////////////////////////////////////////////////////////////////////////////
  {
    MutexLocker mu(AdapterHandlerLibrary_lock);
    // 主要初始化AdapterHandlerLibrary::_adapters属性
    initialize(); 

    if (method->is_abstract()) {
       return _abstract_method_handler;
    }


    int total_args_passed = method->size_of_parameters(); 

    // 宏扩展后为:(BasicType*) resource_allocate_bytes(  (total_args_passed) * sizeof(BasicType)  )
    // BasicType为枚举类型,在之前介绍调用约定时详细介绍过
    BasicType* sig_bt = NEW_RESOURCE_ARRAY(BasicType, total_args_passed);
    // 宏扩展后为:(VMRegPair*) resource_allocate_bytes(  (total_args_passed) * sizeof(VMRegPair)  )
    VMRegPair* regs   = NEW_RESOURCE_ARRAY(VMRegPair, total_args_passed);

    int i = 0;
    if (!method->is_static()){  
      sig_bt[i++] = T_OBJECT; // 实例方法传递的第一个参数为this指针
    }

    for (SignatureStream ss(method->signature()); !ss.at_return_type(); ss.next()) {
      sig_bt[i++] = ss.type();  
      if (ss.type() == T_LONG || ss.type() == T_DOUBLE){
        sig_bt[i++] = T_VOID;   // 对于Long和Double类型来说,占用2个slot
      }
    }
    assert(i == total_args_passed, "");
    // 针对不同的方法签名生成不同的AdapterHandlerEntry,所以需要一个AdapterHandlerTable容器来存储
    // 这样,相同的方法签名就可以重用AdapterHandlerEntry,如果查找了,直接返回就可以
    entry = _adapters->lookup(total_args_passed, sig_bt);
    if (entry != NULL) {
      return entry;
    }

    // 根据java编译执行的调用约定计算出需要通过栈来传递的参数的栈大小
    int comp_args_on_stack = SharedRuntime::java_calling_convention(sig_bt, regs, total_args_passed, false);

    fingerprint = new AdapterFingerPrint(total_args_passed, sig_bt);

    // 为AdapterHandlerEntry中的_i2c_entry、_c2i_entry等生成对应的stub

    BufferBlob* buf = buffer_blob(); 
    if (buf != NULL) {
      CodeBuffer      buffer(buf);
      short           buffer_locs[20];
      CodeSection*    csn = buffer.insts();
      csn->initialize_shared_locs( (relocInfo*)buffer_locs, sizeof(buffer_locs)/sizeof(relocInfo) );
      MacroAssembler  _masm(&buffer);

      entry = SharedRuntime::generate_i2c2i_adapters(
								 &_masm,
								 total_args_passed,
								 comp_args_on_stack,
								 sig_bt,
								 regs,
								 fingerprint
							  );

      B = AdapterBlob::create(&buffer); // 创建了AdapterBlob对象
    } // 结束  buf != NULL


    address ctmp = B->content_begin();
    entry->relocate(ctmp);

    _adapters->add(entry);
  }
  //////////////////////////////////////////////////////////////////////////////////////////

  return entry;
}

这里我们需要介绍3个知识点:

第1个就是AdapterHandlerLibrary::_adapters属性,类型为AdapterHandlerTable*,这是一个容器,存储着许多AdapterHandlerEntry,如果容器的同一个位置有多个AdapterHandlerEntry,则使用单链表连接起来,而AdapterHandlerEntry中定义了如下3个属性:

AdapterFingerPrint* _fingerprint;
address _i2c_entry;
address _c2i_entry;
address _c2i_unverified_entry;

其中的第1个属性是为了重用i2c stub、c2i stub的,因为方法参数不同,每次需要生成的stub也不同,但是如果方法相同,这些stub是可以重用的,所以使用AdapterHandlerTable来重用stub。每次在生成stub之前,查看一下AdapterHandlerTable是否有可重用的stub,如果没有就需要生成。

第2个需要介绍的就是生成AdapterBlob,相关的机器代码首先会生成到CodeBuffer中,最终会通过AdapterBlob来存储,之前为字节码生成机器码时,也是首先生成到CodeBuffer中,然后通过BufferBlob来存储。之所以要这样做,就是为了在内存中将生成的各个Stub放的整齐一些,一旦放整齐了就不会再移动,除非被卸载。

第3个知识点最重要,调用SharedRuntime::generate_i2c2i_adapters()函数生成i2c stub和c2i stub,这个函数共生成3个机器片段,其入口用AdapterHandlerEntry类中的如上3个属性保存。下一篇将详细介绍。

参考文章:

(1)HotSpot中执行引擎技术详解(一)——分阶段代码执行 

(2)https://hllvm-group.iteye.com/group/topic/37707

 公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

posted on 2022-02-11 08:38  鸠摩(马智)  阅读(577)  评论(0编辑  收藏  举报

导航