JVM之创建对象源码分析

1、背景

上文探讨了:【JVM】模板解释器–如何根据字节码生成汇编码?

本篇,我们来关注下字节码的resolve过程。

闲来无事,编译调试了下OpenJDK9,仔细研究了下HotSpot中的模板解释器。

之前对Java对象的创建一直都是概念上的了解,没有在源码层面进行过分析,这段时间在看HotSpot,就顺便了解了下JVM究竟是如何创建Java对象的。

2、问题及准备工作

上文虽然探讨了字节码到汇编码的过程,但是:

mov %rax,%(rcx,rbx,1) // 0x89 0x04 0x19

其中为什么要指定0×04和0×19呢?

搬出我们的代码:

public int swap2(CallBy a,CallBy b) {
    int t = a.value;
    a.value = b.value;
    b.value  = t;
    return t;
}

换句话讲,我们的汇编代码是要将b.value赋给a.value:

//b.value怎么来的呢?
a.value = b.value

b.value是个整形的field,上述代码的关键字节码是putfield,而模板解释器在初始化的时候(非运行时,这也是模板的意义所在)会调用下面的函数来生成对应的汇编码:

void TemplateTable::putfield_or_static(int byte_no, bool is_static) {
  transition(vtos, vtos);

  const Register cache = rcx;
  const Register index = rdx;
  const Register obj   = rcx;
  const Register off   = rbx;
  const Register flags = rax;
  const Register bc    = c_rarg3;

  /********************************
  * 关键:这个函数在做什么?
  ********************************/
  resolve_cache_and_index(byte_no, cache, index, sizeof(u2));

  jvmti_post_field_mod(cache, index, is_static);

  // 上面resolve后,直接从cp cache中对应的entry中就可以获取到field
  load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);

  // [jk] not needed currently
  // volatile_barrier(Assembler::Membar_mask_bits(Assembler::LoadStore |
  //                                              Assembler::StoreStore));

  Label notVolatile, Done;
  __ movl(rdx, flags);
  __ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);
  __ andl(rdx, 0x1);

  // field address
  const Address field(obj, off, Address::times_1);

  Label notByte, notInt, notShort, notChar,
        notLong, notFloat, notObj, notDouble;

  __ shrl(flags, ConstantPoolCacheEntry::tos_state_shift);

  assert(btos == 0, "change code, btos != 0");
  __ andl(flags, ConstantPoolCacheEntry::tos_state_mask);
  __ jcc(Assembler::notZero, notByte);

  // btos
  // ...

  // atos
  // ...

  // itos
  {

    /***************************************
    *  itos类型,我们的b.value是个整形,
    *  所以对应的机器级别的类型是i,表示整形
    ****************************************/

    __ pop(itos);
    if (!is_static) pop_and_check_object(obj);

    // 这里就是生成汇编码,也就是上篇博文探讨的主要内容了
    __ movl(field, rax);

    if (!is_static) {
      patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no);
    }
    __ jmp(Done);
  }

  __ bind(notInt);
  __ cmpl(flags, ctos);
  __ jcc(Assembler::notEqual, notChar);

  // ctos
  // ...

  // stos
  // ...

  // ltos
  // ...

  // ftos
  // ...

  // dtos
  // ...

  // Check for volatile store
  // ...
}

C和C++之类的语言,会在编译期就直接编译成平台相关的机器指令,对于不同平台,可执行文件类型也不一样,如Linux为ELF,Windows为PE,而MacOS为Mach-O。而写Java的应该都清楚,java之所以跨平台性比较强,是因为Java在编译期没有被直接编译成机器指令,而是被编译成一种中间语言:字节码。

  1. 检查对象所属类是否已经被加载解析;
  2. 为对象分配内存空间;
  3. 将分配给对象的内存初始化为零值;
  4. 执行对象的<init>方法,用来初始化对象。

3、field、class的符号解析及链接

2016年我读完周志明的《深入理解Java虚拟机》后,不觉过瘾,便紧接着看完了张秀宏老师的《自己动手写Java虚拟机》,书中关于如何实现一个小型JVM做了详细讲解,其中一部分就是讲如何执行Class文件中方法体的字节码。

我之前收藏过一张图,忘记出处了,此处引用下:

3.1、resolve_cache_and_index

来看看上面代码中的关键点:

// 1. 根据不同的字节码,选择对应的resolve函数.
// 2. 调用resolve函数.
// 3. 根据resolve后的结果,更新寄存器信息,做好衔接.
void TemplateTable::resolve_cache_and_index(int byte_no,
                                            Register Rcache,
                                            Register index,
                                            size_t index_size) {
  const Register temp = rbx;
  assert_different_registers(Rcache, index, temp);

  Label resolved;
    assert(byte_no == f1_byte || byte_no == f2_byte, "byte_no out of range");

    /****************
    * 关键点1
    *****************/

    __ get_cache_and_index_and_bytecode_at_bcp(Rcache, index, temp, byte_no, 1, index_size);
    __ cmpl(temp, (int) bytecode());  // have we resolved this bytecode?
    __ jcc(Assembler::equal, resolved);

  // resolve first time through
  address entry;
  switch (bytecode()) {
  case Bytecodes::_getstatic:
  case Bytecodes::_putstatic:
  case Bytecodes::_getfield:
  case Bytecodes::_putfield:

    /****************
    * 关键点2
    *****************/

    entry = CAST_FROM_FN_PTR(address, InterpreterRuntime::resolve_get_put);
    break;

  // ...

  default:
    fatal(err_msg("unexpected bytecode: %s", Bytecodes::name(bytecode())));
    break;
  }

  // 
  __ movl(temp, (int) bytecode());
  __ call_VM(noreg, entry, temp);

  //
  // Update registers with resolved info
  __ get_cache_and_index_at_bcp(Rcache, index, 1, index_size);
  __ bind(resolved);
}

上面的代码又有两个关键点:

《自己动手写Java虚拟机》中对于字节码的执行其实就是简单的翻译,比如要实现iload指令(将指定的
int
型局部变量推送至栈顶),其实就用GO(这本书用GO来实现JVM的)来实现其对应的功能:

2.1:了解对象创建指令

3.2、get_cache_and_index_and_bytecode_at_bcp

get_cache_and_index_and_bytecode_at_bcp函数,主要做的一些工作如下文所述。

cp
cache指ConstantPoolCache,注意这不是一个一般意义上的缓存,其目的是用于解释器执行时,对字节码进行resolve的。

  1. 对给定的bytecode,在cp
    cache中查找是否已经存在,如果不存在要进行resolve.至于cp
    cache问题,最后再说。
  2. 进行resolve的主要内容:
    – InterpreterRuntime::resolve_get_put
    – InterpreterRuntime::resolve_invoke
    – InterpreterRuntime::resolve_invokehandle
    – InterpreterRuntime::resolve_澳门新浦京娱乐游戏 ,invokedynamic
func _iload(frame *rtda.Frame, index uint) { val := frame.LocalVars().GetInt frame.OperandStack().PushInt}

我们先从一个简单的Demo入手:

3.3、resolve_get_put

因为我们的putfield字节码会选择函数resolve_get_put来进行resolve,来关注这个过程:

IRT_ENTRY(void, InterpreterRuntime::resolve_get_put(JavaThread* thread, Bytecodes::Code bytecode))
  // resolve field
  fieldDescriptor info;
  constantPoolHandle pool(thread, method(thread)->constants());
  bool is_put    = (bytecode == Bytecodes::_putfield  || bytecode == Bytecodes::_putstatic);
  bool is_static = (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic);

  {
    JvmtiHideSingleStepping jhss(thread);

    /*******************
    * 关键点
    ********************/

    LinkResolver::resolve_field_access(info, pool, get_index_u2_cpcache(thread, bytecode),
                                       bytecode, CHECK);
  } // end JvmtiHideSingleStepping

  // check if link resolution caused cpCache to be updated
  if (already_resolved(thread)) return;

  // compute auxiliary field attributes
  TosState state  = as_TosState(info.field_type());

  Bytecodes::Code put_code = (Bytecodes::Code)0;

  InstanceKlass* klass = InstanceKlass::cast(info.field_holder());
  bool uninitialized_static = ((bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic) &&
                               !klass->is_initialized());
  Bytecodes::Code get_code = (Bytecodes::Code)0;

  if (!uninitialized_static) {
    get_code = ((is_static) ? Bytecodes::_getstatic : Bytecodes::_getfield);
    if (is_put || !info.access_flags().is_final()) {
      put_code = ((is_static) ? Bytecodes::_putstatic : Bytecodes::_putfield);
    }
  }

  // 设置cp cache entry
  // 1. field的存/取字节码.
  // 2. field所属的InstanceKlass(Java类在VM层面的抽象)指针.
  // 3. index和offset
  // 4. field在机器级别的类型状态.因为机器级别只有i(整)、a(引用)、v(void)等类型,这一点也可以帮助理解为什么解释器在生成汇编代码时,需要判断tos.
  // 5. field是否final的.
  // 6. field是否volatile的.
  // 7. 常量池的holder(InstanceKlass*类型).
  cache_entry(thread)->set_field(
    get_code,
    put_code,
    info.field_holder(),
    info.index(),
    info.offset(),
    state,
    info.access_flags().is_final(),
    info.access_flags().is_volatile(),
    pool->pool_holder()
  );
IRT_END

注意tos这个点:

其中,tos是指 T op– O f– S tack,也就是操作数栈(vm实现中是expression
stack)顶的东东的类型.

上面的代码中又标出一个关键点:

当执行方法中的iload指令时,就直接调用该_iload()方法即可。

public class Test { public static void main(String[] args) { Dog dog = new Dog(); } static class Dog{ int age; }}

3.4、resolve_field_access

看代码:

// 对field进行resolve,并检查其可访问性等信息
void LinkResolver::resolve_field_access(fieldDescriptor& result, constantPoolHandle pool, int index, Bytecodes::Code byte, TRAPS) {
  // Load these early in case the resolve of the containing klass fails

  // 从常量池中获取field符号
  Symbol* field = pool->name_ref_at(index);

  // 从常量池中获取field的签名符号
  Symbol* sig   = pool->signature_ref_at(index);

  // resolve specified klass
  KlassHandle resolved_klass;

  // 关键点1
  resolve_klass(resolved_klass, pool, index, CHECK);

  // 关键点2
  KlassHandle  current_klass(THREAD, pool->pool_holder());
  resolve_field(result, resolved_klass, field, sig, current_klass, byte, true, true, CHECK);
}

注意到上面的代码还调用了resolve_klassresolve_field,我们一个一个看,

这种解释器简单明了,而且容易理解,要是让我们来实现虚拟机,估计想到的也是这种方法(虽然我没有那个能力)。早期的HotSpot就是通过上面这种方法来解释执行字节码指令的,这种解释器有个通用的称呼:字节码解释器。目前HotSpot中还保留着字节码解释器,只不过没有使用了。

上面代码很简单,在main()中创建了一个Dog对象,写这个Demo是为了让我们看看new
Dog()在编译成字节码后会变成什么。

3.5、resolve_klass:

// resolve klass
void LinkResolver::resolve_klass(KlassHandle& result, constantPoolHandle pool, int index, TRAPS) {
  Klass* result_oop = pool->klass_ref_at(index, CHECK);
  result = KlassHandle(THREAD, result_oop);
}

上面的代码很简单,从常量池取出对应的klass,并同当前线程一起,封装为一个KlassHandle。

字节码解释器的优点上面已经说过了,但是缺点也很明显:慢。每个字节码指令都要通过翻译执行,虽然在用C++写成的JVM中,类似上面_iload()这样的方法,最后也会被编译成机器指令,但是编译器生成的机器指令很冗余,而CPU本身就是不断取指执行,指令越多,耗时也就越长。对于JVM的解释器来说,其实也就是不断取指执行,如果每个字节码指令的执行时间都很慢,那么整体效率必然很差。

public static void main(java.lang.String[]); Code: 0: new #2 // class com/wangxiandeng/test/Test$Dog 3: dup 4: invokespecial #3 // Method com/wangxiandeng/test/Test$Dog."<init>":()V 7: astore_1 8: return}

3.6、resolve_field:

再接着看resolve_field:

// field的解析及链接
// 此过程将完成:
//
//   1. field的可访问性验证.
//   2. field所属的类的可访问性验证.
//   3. field所属的类的ClassLoaderData及当前执行的方法(Method)所属的类的ClassLoaderData的验证.
//   4. field所属的类中,如果对其它的类有依赖,要进行装载、解析和链接,如果没有找到,比如classpath中不包含,那么就报类似ClassDefNotFoundError的异常.
//    如果Jar包冲突,也在这里检测到,并报异常.
//    如果field所属的类,及其依赖的类都找到了,那么将ClassLoaderData的约束constraint进行合并.
//   5. 当前正在调用的方法的签名,从callee角度和caller角度来比较是否一致.

// 关于classLoader的问题,后续文章再展开吧,不是一句两句能说的清。
void LinkResolver::resolve_field(fieldDescriptor& fd, KlassHandle resolved_klass, Symbol* field, Symbol* sig,
                                 KlassHandle current_klass, Bytecodes::Code byte, bool check_access, bool initialize_class,
                                 TRAPS) {
  assert(byte == Bytecodes::_getstatic || byte == Bytecodes::_putstatic ||
         byte == Bytecodes::_getfield  || byte == Bytecodes::_putfield  ||
         (byte == Bytecodes::_nop && !check_access), "bad field access bytecode");

  bool is_static = (byte == Bytecodes::_getstatic || byte == Bytecodes::_putstatic);
  bool is_put    = (byte == Bytecodes::_putfield  || byte == Bytecodes::_putstatic);

  // Check if there's a resolved klass containing the field
  if (resolved_klass.is_null()) {
    ResourceMark rm(THREAD);
    THROW_MSG(vmSymbols::java_lang_NoSuchFieldError(), field->as_C_string());
  }

  /************************
  * 关键点1
  *************************/
  // Resolve instance field
  KlassHandle sel_klass(THREAD, resolved_klass->find_field(field, sig, &fd));

  // check if field exists; i.e., if a klass containing the field def has been selected
  if (sel_klass.is_null()) {
    ResourceMark rm(THREAD);
    THROW_MSG(vmSymbols::java_lang_NoSuchFieldError(), field->as_C_string());
  }

  if (!check_access)
    // Access checking may be turned off when calling from within the VM.
    return;

  /************************
  * 关键点2
  *************************/
  // check access
  check_field_accessability(current_klass, resolved_klass, sel_klass, fd, CHECK);

  // check for errors
  if (is_static != fd.is_static()) {

    // ...

    THROW_MSG(vmSymbols::java_lang_IncompatibleClassChangeError(), msg);
  }

  // Final fields can only be accessed from its own class.
  if (is_put && fd.access_flags().is_final() && sel_klass() != current_klass()) {
    THROW(vmSymbols::java_lang_IllegalAccessError());
  }

  // initialize resolved_klass if necessary
  // note 1: the klass which declared the field must be initialized (i.e, sel_klass)
  //         according to the newest JVM spec (5.5, p.170) - was bug (gri 7/28/99)
  //
  // note 2: we don't want to force initialization if we are just checking
  //         if the field access is legal; e.g., during compilation
  if (is_static && initialize_class) {
    sel_klass->initialize(CHECK);
  }

  if (sel_klass() != current_klass()) {
    HandleMark hm(THREAD);
    Handle ref_loader (THREAD, InstanceKlass::cast(current_klass())->class_loader());
    Handle sel_loader (THREAD, InstanceKlass::cast(sel_klass())->class_loader());
    {
      ResourceMark rm(THREAD);

      /************************
      * 关键点3
      *************************/
      Symbol* failed_type_symbol =
        SystemDictionary::check_signature_loaders(sig,
                                                  ref_loader, sel_loader,
                                                  false,
                                                  CHECK);
      if (failed_type_symbol != NULL) {

        // ...

        THROW_MSG(vmSymbols::java_lang_LinkageError(), buf);
      }
    }
  }

  // return information. note that the klass is set to the actual klass containing the
  // field, otherwise access of static fields in superclasses will not work.
}

上面的代码,我们梳理出三个跟本主题相关的关键点,已在注释中标出,我们来看:

// 关键点1 :
// 获取field所属的类或接口对应的klass,或者NULL,如果是NULL就抛异常了
KlassHandle sel_klass(THREAD, resolved_klass->find_field(field, sig, &fd));

// 1. 如果是resolved_klass中的field,返回resolved_klass
// 2. 如果1不满足,尝试返回接口或接口的超类(super interface)对应的klass(递归)
// 3. 如果1、2点都不满足,尝试返回父类或超类对应的klass(递归)或者NULL.
Klass* InstanceKlass::find_field(Symbol* name, Symbol* sig, fieldDescriptor* fd) const {
  // search order according to newest JVM spec (5.4.3.2, p.167).
  // 1) search for field in current klass
  if (find_local_field(name, sig, fd)) {
    return const_cast<InstanceKlass*>(this);
  }
  // 2) search for field recursively in direct superinterfaces
  { Klass* intf = find_interface_field(name, sig, fd);
    if (intf != NULL) return intf;
  }
  // 3) apply field lookup recursively if superclass exists
  { Klass* supr = super();
    if (supr != NULL) return InstanceKlass::cast(supr)->find_field(name, sig, fd);
  }
  // 4) otherwise field lookup fails
  return NULL;
}

// 关键点2:
// 1. resolved_klass来自当前线程所执行的当前方法的当前字节码所属的常量池.
// 2. sel_klass是field所属的类或接口对应的klass
// 3. current_klass是常量池所属的klass(pool_holder).
// 4. 3种klass可以相同,也可以不同.可以想象一个调用链,依赖的各个class.
check_field_accessability(current_klass, resolved_klass, sel_klass, fd, CHECK);

// 关键点3:
// ref_loader代表了current_klass的classLoader
Handle ref_loader (THREAD, InstanceKlass::cast(current_klass())->class_loader());
// sel_loader代表了sel_klass的classLoader
    Handle sel_loader (THREAD, InstanceKlass::cast(sel_klass())->class_loader());
// 根据签名符号sig、ref_loader、sel_loader来检查classLoader的约束是否一致,如果不一致就会抛异常,所谓一致不是相同但包含相同的情况,如果一致,那么就合并约束,同时还要进行依赖(depedencies)链的维护.
// 由于内容比较多,本篇不展开.
Symbol* failed_type_symbol =
        SystemDictionary::check_signature_loaders(sig,
                                                  ref_loader, sel_loader,
                                                  false,
                                                  CHECK);

上面的关键点解析都在注释中了,其中有的地方内容太多,不宜在本篇展开。

那么,如何获取当前执行的字节码对应的cp cache entry呢?

早期的字节码解释器既然已经不能适应时代的发展,那么JVM的工程师想出了什么优化呢?上面提到字节码解释器慢是因为编译器生成的机器指令不够理想,那么我们直接跳过编译器,自己动手写汇编代码不就行了。没错,现在的HotSpot就是这样干的,这种解释器便称为模板解释器。

可见new Dog()变成了new #2,new
是java众多字节码中用来实例化对象的字节码,不用我说大家肯定也清楚,关键后面的
#2 是个啥?

3.7、如何获取cp cache entry:

关键代码如下:

// 获取当前正在执行的bytecode对应的cp cache entry
static ConstantPoolCacheEntry* cache_entry(JavaThread *thread) { 
  return cache_entry_at(thread, Bytes::get_native_u2(bcp(thread) + 1)); 
}

// ↓

// 获取解释器当前的(B)yte (C)ode (P)ointer,也就是当前指令地址,以指针表达
static address   bcp(JavaThread *thread)           { 
  return last_frame(thread).interpreter_frame_bcp(); 
}

// ↓

// 获取cp cache entry
static ConstantPoolCacheEntry* cache_entry_at(JavaThread *thread, int i)  { 
  return method(thread)->constants()->cache()->entry_at(i); 
}

// ↓

// 获取当前正在执行的方法
static Method*   method(JavaThread *thread) { 
  return last_frame(thread).interpreter_frame_method(); 
}

// ↓

// 获取interpreterState->_method,也就是当前正在执行的方法
Method* frame::interpreter_frame_method() const {
  assert(is_interpreted_frame(), "interpreted frame expected");
  Method* m = *interpreter_frame_method_addr();
  assert(m->is_method(), "not a Method*");
  return m;
}

// ↓

// 获取interpreterState->_method的地址
inline Method** frame::interpreter_frame_method_addr() const {
  assert(is_interpreted_frame(), "must be interpreted");
  return &(get_interpreterState()->_method);
}

// ↓

// 获取interpreterState
inline interpreterState frame::get_interpreterState() const {
  return ((interpreterState)addr_at( -((int)sizeof(BytecodeInterpreter))/wordSize ));
}

// ↓

// interpreterState实际是个BytecodeInterpreter型指针
typedef class BytecodeInterpreter* interpreterState;

上述过程总结下:

1、获取bcp,也就是解释器当前正在执行的字节码的地址,以指针形式返回.

2、bcp是通过当前线程的调用栈的最后一帧来获取的,并且是个解释器栈帧.为什么是最后一帧?

方法1 栈帧1 
调用 -> 方法2 栈帧2
...
调用 -> 方法n 栈帧n // 最后一帧

每个方法在调用时都会用一个栈帧frame来描述调用的状态信息,最后调用的方法就是当前方法,所以是取最后一帧.

3、当前方法的地址是通过栈帧中保存的interpreterState来获取的,而这个interpreterState是个BytecodeInterpreter型的解释器,不是模板解释器。

4、获取到方法的地址后,就可以获取到方法所属的常量池了,接着从常量池对应的cp
cache中就可以获取到对应的entry了。

5、第4点提到对应,怎么个对应法?想象数组的下标,这个下标是什么呢?就是对bcp的一个整形映射。

模板解释器相对于为每一个指令都写了一段实现对应功能的汇编代码,在JVM初始化时,汇编器会将汇编代码翻译成机器指令加载到内存中,比如执行iload指令时,直接执行对应的汇编代码即可。如何执行汇编代码?直接跳往汇编代码生成的机器指令在内存中的地址即可。HotSpot中很多地方都是利用手动汇编代码来优化效率的,在我的文章《JVM方法执行的来龙去脉》中也提到,方法的调用也是通过手动汇编代码来执行的。

类在编译成字节码时,会生成类所属的常量池,常量池中记录了各种符号引用及常量,#2
其实就是常量池中索引为2的常量项,此处指向的是Dog类的符号。

3.8、BytecodeInterpreter的一些关键字段

注意BytecodeInterpreter和TemplateInterpreter不是一码事.

BytecodeInterpreter的一些关键字段,帮助理解bcp、thread、cp、cp
cache在解释器栈帧中意义:

private:
    JavaThread*           _thread;        // the vm's java thread pointer
    address               _bcp;           // instruction pointer
    intptr_t*             _locals;        // local variable pointer
    ConstantPoolCache*    _constants;     // constant pool cache
    Method*               _method;        // method being executed
    DataLayout*           _mdx;           // compiler profiling data for current bytecode
    intptr_t*             _stack;         // expression stack
    messages              _msg;           // frame manager <-> interpreter message
    frame_manager_message _result;        // result to frame manager
    interpreterState      _prev_link;     // previous interpreter state
    oop                   _oop_temp;      // mirror for interpreted native, null otherwise
    intptr_t*             _stack_base;    // base of expression stack
    intptr_t*             _stack_limit;   // limit of expression stack
    BasicObjectLock*      _monitor_base;  // base of monitors on the native stack

在进行resolve后,字节码就在ConstantPoolCache对应的Entry中了,下一次再执行就不需要resolve。

至于BytecodeInterpreter是个什么解释器,和模板解释器有啥关系,后面再说吧。

1:模板的初始化及机器指令的生成

2.2:new指令源码分析

4、结语

本文简要探讨了:

字节码的resolve过程。

我们平时说的iload指令等,其实都只是字节码指令的助记符,帮助我们理解,真正的自己码指令其实就是一个数字,比如iload是21,虚拟机执行21这个指令时,就是执行iload。字节码指令定义在bytecodes.hpp中:

上面已经对类创建的字节码进行了简单介绍,我们已经知道了用于对象创建的字节码指令为new,接下来就可以对new指令进行源码分析了。

class Bytecodes: AllStatic { public: enum Code { _illegal = -1, // Java bytecodes _nop = 0, // 0x00 _aconst_null = 1, // 0x01 _iconst_m1 = 2, // 0x02 _iconst_0 = 3, // 0x03 _iconst_1 = 4, // 0x04 _iconst_2 = 5, // 0x05 _iconst_3 = 6, // 0x06 _iconst_4 = 7, // 0x07 _iconst_5 = 8, // 0x08 _lconst_0 = 9, // 0x09 ...... }}

在我上一篇博客中对java字节码指令的运行进行了介绍,主要讲的是模板解释器,今天我们仍然对模板解释器中new指令的运行进行讲解,不清楚模板解释器的读者可以看看《JVM之模板解释器》。

JVM初始化时会为每个自己码指令都创建一个模板,每个模板都关联其对应的汇编代码生成函数:

我们先来看看new指令对应的汇编代码,代码很长,我们稍后会进行逐步分析,不想直接看代码的同学可以先跳过。

void TemplateTable::initialize() { ...... def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ ); def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ ); def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 ); ...... def(Bytecodes::_iload , ubcp|____|clvm|____, vtos, itos, iload , _ ); ......}
/hotspot/src/cpu/x86/vm/templateTable_x86.cppvoid TemplateTable::_new() { transition(vtos, atos); __ get_unsigned_2_byte_index_at_bcp; Label slow_case; Label slow_case_no_pop; Label done; Label initialize_header; Label initialize_object; // including clearing the fields Label allocate_shared; __ get_cpool_and_tags; // Make sure the class we're about to instantiate has been resolved. // This is done before loading InstanceKlass to be consistent with the order // how Constant Pool is updated (see ConstantPool::klass_at_put) const int tags_offset = Array<u1>::base_offset_in_bytes(); __ cmpb(Address(rax, rdx, Address::times_1, tags_offset), JVM_CONSTANT_Class); __ jcc(Assembler::notEqual, slow_case_no_pop); // get InstanceKlass __ movptr(rcx, Address(rcx, rdx, Address::times_ptr, sizeof(ConstantPool))); __ push; // save the contexts of klass for initializing the header // make sure klass is initialized & doesn't have finalizer // make sure klass is fully initialized __ cmpb(Address(rcx, InstanceKlass::init_state_offset, InstanceKlass::fully_initialized); __ jcc(Assembler::notEqual, slow_case); // get instance_size in InstanceKlass (scaled to a count of bytes) __ movl(rdx, Address(rcx, Klass::layout_helper_offset; // test to see if it has a finalizer or is malformed in some way __ testl(rdx, Klass::_lh_instance_slow_path_bit); __ jcc(Assembler::notZero, slow_case); // // Allocate the instance // 1) Try to allocate in the TLAB // 2) if fail and the object is large allocate in the shared Eden // 3) if the above fails (or is not applicable), go to a slow case // (creates a new TLAB, etc.) const bool allow_shared_alloc = Universe::heap()->supports_inline_contig_alloc(); const Register thread = LP64_ONLY(r15_thread) NOT_LP64;#ifndef _LP64 if (UseTLAB || allow_shared_alloc) { __ get_thread; }#endif // _LP64 if  { __ movptr(rax, Address(thread, in_bytes(JavaThread::tlab_top_offset; __ lea(rbx, Address(rax, rdx, Address::times_1)); __ cmpptr(rbx, Address(thread, in_bytes(JavaThread::tlab_end_offset; __ jcc(Assembler::above, allow_shared_alloc ? allocate_shared : slow_case); __ movptr(Address(thread, in_bytes(JavaThread::tlab_top_offset; if  { // the fields have been already cleared __ jmp(initialize_header); } else { // initialize both the header and fields __ jmp(initialize_object); } } // Allocation in the shared Eden, if allowed. // // rdx: instance size in bytes if (allow_shared_alloc) { __ bind(allocate_shared); ExternalAddress heap_topUniverse::heap()->top_addr; ExternalAddress heap_endUniverse::heap()->end_addr; Label retry; __ bind; __ movptr(rax, heap_top); __ lea(rbx, Address(rax, rdx, Address::times_1)); __ cmpptr(rbx, heap_end); __ jcc(Assembler::above, slow_case); // Compare rax, with the top addr, and if still equal, store the new // top addr in rbx, at the address of the top addr pointer. Sets ZF if was // equal, and clears it otherwise. Use lock prefix for atomicity on MPs. // // rax,: object begin // rbx,: object end // rdx: instance size in bytes __ locked_cmpxchgptr(rbx, heap_top); // if someone beat us on the allocation, try again, otherwise continue __ jcc(Assembler::notEqual, retry); __ incr_allocated_bytes(thread, rdx, 0); } if (UseTLAB || Universe::heap()->supports_inline_contig_alloc { // The object is initialized before the header. If the object size is // zero, go directly to the header initialization. __ bind(initialize_object); __ decrement(rdx, sizeof; __ jcc(Assembler::zero, initialize_header); // Initialize topmost object field, divide rdx by 8, check if odd and // test if zero. __ xorl; // use zero reg to clear memory (shorter code) __ shrl(rdx, LogBytesPerLong); // divide by 2*oopSize and set carry flag if odd // rdx must have been multiple of 8#ifdef ASSERT // make sure rdx was multiple of 8 Label L; // Ignore partial flag stall after shrl() since it is debug VM __ jccb(Assembler::carryClear, L); __ stop("object size is not multiple of 2 - adjust this code"); __ bind; // rdx must be > 0, no extra check needed here#endif // initialize remaining object fields: rdx was a multiple of 8 { Label loop; __ bind; __ movptr(Address(rax, rdx, Address::times_8, sizeof - 1*oopSize), rcx); NOT_LP64(__ movptr(Address(rax, rdx, Address::times_8, sizeof - 2*oopSize), rcx)); __ decrement; __ jcc(Assembler::notZero, loop); } // initialize object header only. __ bind(initialize_header); if (UseBiasedLocking) { __ pop; // get saved klass back in the register. __ movptr(rbx, Address(rcx, Klass::prototype_header_offset; __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ; } else { __ movptr(Address(rax, oopDesc::mark_offset_in_bytes , markOopDesc::prototype; // header __ pop; // get saved klass back in the register. }#ifdef _LP64 __ xorl; // use zero reg to clear memory (shorter code) __ store_klass_gap; // zero klass gap for compressed oops#endif __ store_klass; // klass { SkipIfEqual skip_if(_masm, &DTraceAllocProbes, 0); // Trigger dtrace event for fastpath __ push; __ call_VM_leaf( CAST_FROM_FN_PTR(address, SharedRuntime::dtrace_object_alloc), rax); __ pop; } __ jmp; } // slow case __ bind(slow_case); __ pop; // restore stack pointer to what it was when we came in. __ bind(slow_case_no_pop); Register rarg1 = LP64_ONLY NOT_LP64; Register rarg2 = LP64_ONLY NOT_LP64; __ get_constant_pool; __ get_unsigned_2_byte_index_at_bcp; call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2); __ verify_oop; // continue __ bind;}

def()函数其实就是用来创建模板的:

下面我们来逐步看看上面这些代码主要干了啥。1:获取new指令后的操作数,即类在常量池的索引,放入rdx寄存器中。bcp即rsi寄存器,用来记录当前解释器运行的字节码指令地址,类似SS:IP寄存器,用来进行pc计数。这个方法主要就是获取当前运行指令地址偏移一个字节处内容。

void TemplateTable::def(Bytecodes::Code code, int flags, TosState in, TosState out, void , int arg) { ...... Template* t = is_wide ? template_for_wide : template_for; // setup entry t->initialize(flags, in, out, gen, arg);}
__ get_unsigned_2_byte_index_at_bcp;

在调用def()时,我们传入了一系列参数,其中倒数第二个参数为一个函数指针,其实这个函数指针指向的就是字节码指令对应的汇编代码生成函数。我们还是拿iload指令说事吧,在创建iload指令模板时,传入的函数指针为iload:

2:获取常量池首地址放入rcx寄存器,获取常量池中元素类型数组_tags首地址,放入rax中。_tags数组按顺序存放了每个常量池元素的类型。

void TemplateTable::iload() { ...... //获取局部变量slot号,放入rbx中 locals_index; //将slot对应的局部变量移动至rax中 __ movl(rax, iaddress;}
__ get_cpool_and_tags;

iload()函数会生成iload指令对应的机器指令。

3:判断_tags数组中对应元素类型是否为JVM_CONSTANT_Class,不是则跳往slow_case_no_pop处。

在定义完成所有字节码对应的模板后,JVM会遍历所有字节码,为每个字节码生成对应的机器指令入口:

const int tags_offset = Array<u1>::base_offset_in_bytes();__ cmpb(Address(rax, rdx, Address::times_1, tags_offset), JVM_CONSTANT_Class);__ jcc(Assembler::notEqual, slow_case_no_pop);
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 { set_entry_points; } else { set_unimplemented; } }}

4:获取创建对象所属类地址,放入rcx中,即类的运行时数据结构InstanceKlass,并将其入栈。

set_entry_points最终会调用TemplateInterpreterGenerator::generate_and_dispatch()来生成机器指令:

__ movptr(rcx, Address(rcx, rdx, Address::times_ptr, sizeof(ConstantPool)));__ push; // save the contexts of klass for initializing the header
void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) { ...... // generate template t->generate; // advance if (t->does_dispatch {#ifdef ASSERT // make sure execution doesn't go beyond this point if code is broken __ should_not_reach_here();#endif // ASSERT } else { // dispatch to next bytecode __ dispatch_epilog(tos_out, step); }}

5:判断类是否已经被解析过,没有解析的话直接跳往slow_close,slow_case即慢速分配,如果对象所属类已经被解析过,则会进入快速分配,否则会进入慢速分配,去进行类的解析。

在generate_and_dispatch()中,会调用模版的generate()方法,因为模板初始化时记录了对应的机器指令生成函数的指针,存在_gen中,所以这里直接调用_gen()即可生成机器指令,对于iload来说,就相当于调用了TemplateTable::iload():

__ cmpb(Address(rcx, InstanceKlass::init_state_offset, InstanceKlass::fully_initialized);__ jcc(Assembler::notEqual, slow_case);
void Template::generate(InterpreterMacroAssembler* masm) { // parameter passing TemplateTable::_desc = this; TemplateTable::_masm = masm; // code generation _gen; masm->flush();}

6:此时rcx中存放的是类InstanceKlass的内存地址,利用偏移获取类实例大小,存入rdx寄存器,对象的大小早在类加载时就已经确定了。

2:字节码派发表的创建

__ movl(rdx, Address(rcx, Klass::layout_helper_offset;

机器指令生成完成后,事情还没结束,因为我们需要记录机器指令的入口地址。在set_entry_points()末尾,会创建一个EntryPoint记录生成的机器指令的入口,并将EntryPoint以字节码为索引,存储到Interpreter::_normal_table表中。注:因为字节码指令本身就是从0开始递增的:_nop
= 0, _aconst_null = 1
,……..。所以这里可以直接根据字节码指令作为索引。

7:尝试在TLAB区为对象分配内存,TLAB即ThreadLocalAllocationBuffers。每个线程都有自己的一块内存区域,用于分配对象,这块内存区域便为TLAB区。这样的好处是在分配内存时,无需对一整块内存进行加锁。TLAB只是在分配对象时的操作属于线程私有,分配的对象对于其他线程仍是可读的。

 // set entry points EntryPoint entry(bep, zep, cep, sep, aep, iep, lep, fep, dep, vep); Interpreter::_normal_table.set_entry(code, entry); Interpreter::_wentry_point[code] = wep;
if  { // 获取TLAB区剩余空间首地址,放入rax寄存器。 __ movptr(rax, Address(thread, in_bytes(JavaThread::tlab_top_offset; // rdx寄存器已经记录了对象大小,此处及根据TLAB空闲区首地址,计算出对象分配后,对象尾地址,放入rbx中 __ lea(rbx, Address(rax, rdx, Address::times_1)); // 将rbx中内容与TLAB空闲区尾地址进行比较。 __ cmpptr(rbx, Address(thread, in_bytes(JavaThread::tlab_end_offset; // 如果上面比较结果表明rbx > TLAB空闲区尾地址,则表明TLAB区空闲区大小不足以分配该对象,那么在allow_shared_alloc(允许在Eden区分配)情况下,就直接跳往Eden区分配内存标号处运行,即第8步 __ jcc(Assembler::above, allow_shared_alloc ? allocate_shared : slow_case); // 因为对象分配后,TLAB区空间变小,此处更新TLAB空闲区首地址为对象尾地址 __ movptr(Address(thread, in_bytes(JavaThread::tlab_top_offset; // 如果TLAB区默认会对回收的空闲区清零,那么就不需要在为对象变量进行清零操作了,直接跳往对象头初始化处运行。有同学可能会问为什么要进行清零操作呢?因为分配的内存可能还保留着上次分配给其他对象时的数据,内存块虽然被回收了,但是之前的数据没有被清除,会污染新对象。 if  { // the fields have been already cleared __ jmp(initialize_header); } else { // initialize both the header and fields __ jmp(initialize_object); }}

其中Entrypoint定义如下:

8:如果在TLAB区分配失败,会直接在Eden区进行分配,具体过程和第7步很像。

EntryPoint::EntryPoint(address bentry, address zentry, address centry, address sentry, address aentry, address ientry, address lentry, address fentry, address dentry, address ventry) { assert(number_of_states == 10, "check the code below"); _entry[btos] = bentry; _entry[ztos] = zentry; _entry[ctos] = centry; _entry[stos] = sentry; _entry[atos] = aentry; _entry[itos] = ientry; _entry[ltos] = lentry; _entry[ftos] = fentry; _entry[dtos] = dentry; _entry[vtos] = ventry;}
if (allow_shared_alloc) { // TLAB区分配失败会跳到这。 __ bind(allocate_shared); // 获取Eden区剩余空间的首地址和结束地址。 ExternalAddress heap_topUniverse::heap()->top_addr; ExternalAddress heap_endUniverse::heap()->end_addr; Label retry; __ bind; // 将空闲区首地址放入rax中,用作对象分配开始处。 __ movptr(rax, heap_top); // 计算对象尾地址,与空闲区尾地址进行比较,内存不足则跳往慢速分配。 __ lea(rbx, Address(rax, rdx, Address::times_1)); __ cmpptr(rbx, heap_end); __ jcc(Assembler::above, slow_case); // rax,: object begin,rax此时记录了对象分配的内存首地址 // rbx,: object end rbx此时记录了对象分配的内存尾地址 // rdx: instance size in bytes rdx记录了对象大小 // 利用CAS操作,更新Eden空闲区首地址为对象尾地址,因为Eden区是线程共用的,所以需要加锁。 __ locked_cmpxchgptr(rbx, heap_top); // if someone beat us on the allocation, try again, otherwise continue __ jcc(Assembler::notEqual, retry); __ incr_allocated_bytes(thread, rdx, 0);}

这里大家会看到很多btos、ztos之类的,这是TosState,即TopOfStackState,其实这描述的是当前栈顶数据的类型,栈顶数据类型不同时,会进入不同的entry。这部分用到的是栈顶缓存技术,可参考《栈顶缓存(Top-of-Stack
Cashing)技术》,大家只要记住,EntryPoint是用来记录机器指令入口地址即可。

9:对象所需内存已经分配好后,就会进行对象的初始化了,先初始化对象实例数据。

3:取指执行过程

// 开始初始化对象处__ bind(initialize_object);// 如果rdx和sizeof大小一样,即对象所需大小和对象头大小一样,则表明对象真正的实例数据内存为0,那么就不需要进行对象实例数据的初始化了,直接跳往对象头初始化处即可。Hotspot中虽然对象头在内存中排在对象实例数据前,但是会先初始化对象实例数据,再初始化对象头。__ decrement(rdx, sizeof;__ jcc(Assembler::zero, initialize_header);// 执行异或,使得rcx为0,为之后给对象变量赋零值做准备__ xorl; // use zero reg to clear memory (shorter code)__ shrl(rdx, LogBytesPerLong); // divide by 2*oopSize and set carry flag if oddLabel L;__ jccb(Assembler::carryClear, L);__ stop("object size is not multiple of 2 - adjust this code");__ bind;// 此处以rdx递减,按字节进行循环遍历对内存,初始化对象实例内存为零值。{ Label loop;__ bind;__ movptr(Address(rax, rdx, Address::times_8, sizeof - 1*oopSize), rcx);NOT_LP64(__ movptr(Address(rax, rdx, Address::times_8, sizeof - 2*oopSize), rcx));__ decrement;__ jcc(Assembler::notZero, loop);}

大家有没有想过,CPU是如何不断的执行指令的?难道有个统一的管理者,不断的取出下一条指令执行?其实代码段被加载到内存后,会放到连续的一块内存区域,每条指令都是线性排在一起的。CPU利用CS:IP寄存器来记录当前指令地址,因为指令都是连续排在一起的,所以当执行完一条指令后,直接根据当前指令长度进行偏移,就可以拿到下一条指令地址,送入IP寄存器,从而实现连续不断的取指。

10:对象实例数据初始化好后,就开始进行对象头的初始化了。

HotSpot借用了这一思想,在每个字节码指令对应生成的机器指令末尾,会插入一段跳转下一条指令的逻辑。这样当前字节码在完成自己的功能后,就会自动取出方法体中排在它后面的下一条指令开始执行。

// 初始化对象头标号处__ bind(initialize_header);// 是否使用偏向锁,大多时一个对象只会被同一个线程访问,所以在对象头中记录获取锁的线程id,下次线程获取锁时就不需要加锁了。if (UseBiasedLocking) { // 第4步中有将类数据InstanceKlass的地址入栈,此时重新出栈,放入rcx寄存器。 __ pop; // 接下来两步将类的偏向锁相关数据移动到对象头部 __ movptr(rbx, Address(rcx, Klass::prototype_header_offset; __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ;} else { __ movptr(Address(rax, oopDesc::mark_offset_in_bytes , markOopDesc::prototype; // header __ pop; // get saved klass back in the register.}// 此时rcx保存了InstanceKlass,rax保存了对象首地址,此处保存对象所属的类数据InstanceKlass放入对象头中,对象头尾oopDesc类型,里面有个_metadata联合体,_metadata中专门有个Klass指针用来指向类所属对象,此处其实就是将InstanceKlass地址放入该指针中。__ store_klass; // klass{ SkipIfEqual skip_if(_masm, &DTraceAllocProbes, 0); // Trigger dtrace event for fastpath __ push; __ call_VM_leaf( CAST_FROM_FN_PTR(address, SharedRuntime::dtrace_object_alloc), rax); __ pop;}__ jmp;

我们回到上面字节码机器指令生成的函数generate_and_dispatch()中:

11:慢速分配,经过上面分析可知,如果类没有被加载解析,会跳到此处执行。

void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) { ...... // generate template t->generate; // advance if (t->does_dispatch {#ifdef ASSERT // make sure execution doesn't go beyond this point if code is broken __ should_not_reach_here();#endif // ASSERT } else { // dispatch to next bytecode __ dispatch_epilog(tos_out, step); }}
 __ bind(slow_case); // 因为第4步有将InsanceKlass入栈,这里用不上,重新出栈,还原栈顶数据。 __ pop; // restore stack pointer to what it was when we came in. __ bind(slow_case_no_pop); Register rarg1 = LP64_ONLY NOT_LP64; Register rarg2 = LP64_ONLY NOT_LP64; // 获取常量池地址,存入rarg1寄存器。 __ get_constant_pool; // 获取new 指令后操作数,即类在常量池中的索引,放入rarg2寄存器。 __ get_unsigned_2_byte_index_at_bcp; // 进入InterpreterRuntime::_new生成的机器指令地址处,开始执行,里面会进行类的加载和对象分配,并将分配的对象地址返回,存入rax寄存器中。 call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2); __ verify_oop; // 创建结束 __ bind;

t->generate后并没有立马撤走,而是会进行dispatch操作,调用 __
dispatch_epilog(tos_out,
step)来进行下一条指令的执行,dispatch_epilog()里面调用的是dispatch_next()方法:

对象的创建到这就结束了,希望大家能对java对象创建有了更多的了解。因为上面拿模板解释器进行讲解的,都是汇编语言,其实大家也可以直接看看字节码解释器中对象创建的方法,比较好理解。本人能力有限,如有错误,请多指正。目前正在研读HotSpot源码,如果有同学比较感兴趣,也可以一起交流,附上微信:wang_atbeijing

void InterpreterMacroAssembler::dispatch_next(TosState state, int step) { load_unsigned_byte(rbx, Address(_bcp_register, step)); // advance _bcp_register increment(_bcp_register, step); dispatch_base(state, Interpreter::dispatch_table;}

load_unsigned_byte()会根据当前指令地址偏移,获取下条指令地址,并通过地址获得指令,放入rbx寄存器。_bcp_register就是rsi寄存器,HotSpot利用rsi寄存器来存储当前指令地址。

取指完成后,调用increment(_bcp_register,
step)来更新rsi寄存器,使其指向下一条指令地址。

dispatch_base(state,
Interpreter::dispatch_table开始进行下一条指令的执行,Interpreter::dispatch_table返回了之前生成的字节码派发表。

void InterpreterMacroAssembler::dispatch_base(TosState state, address* table, bool verifyoop) { ...... lea(rscratch1, ExternalAddresstable)); jmp(Address(rscratch1, rbx, Address::times_8));}

**lea(rscratch1, ExternalAddresstable))
**将存储指令对应的机器指令地址的DispatchTable内存地址放到rscratch1中。

jmp(Address(rscratch1, rbx,
Address::times_8)):
因为DispatchTable中索引直接为字节码指令,从0开始,而rbx现在存的就是下一条指令,所以可以通过(DispatchTable首地址

  • rbx *
    每个地址所占字节)来索引。然后直接利用jmp指令跳往字节码对应机器指令的地址。

到这里模板解释器的大致逻辑就讲完了,主要分为以下几部分:

  1. 为每个字节码创建模板;
  2. 利用模板为每个字节码生成对应的机器指令;
  3. 将每个字节码生成的机器指令地址存储在派发表中;
  4. 在每个字节码生成的机器指令末尾,插入自动跳转下条指令逻辑。

HotSpot真是座宝库,学习HotSpot不仅仅是为了打开虚拟机这个黑匣子,更重要的是学习它的思想,从而将这种思想能为我们所用!

参考:《揭秘Java虚拟机:JVM设计原理与实现》

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图