原创

对象的创建

Java对象创建的流程大概如下:

  1. 检查对象所属类是否已经被加载解析;
  2. 为对象分配内存空间;
  3. 将分配给对象的内存初始化为零值;
  4. 执行对象的方法进行初始化。
    举个例子如下:
public class Test {
    public static void main(String[] args) {
        Test obj = new Test();
    }
}

方法main()对应的Class文件内容如下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #1                  // class com/test/Test
         3: dup
         4: invokespecial #16                 // Method "<init>":()V
         7: astore_1
         8: return

使用new指令来创建Test对象,下面详细介绍一下HotSpot对new指令的处理。

如果当前是解释执行,那么执行new指令其实会执行/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp文件中定义的TemplateTable::_new()方法生成的一段机器码,不过我们后面可以以源代码和汇编的形式来分析执行的逻辑。

方法首先调用InterpreterMacroAssembler::get_unsigned_2_byte_index_at_bcp()方法加载new指令后的操作数,对于如上实例来说,这个值就是常量池的下标索引1。方法的实现如下:

void InterpreterMacroAssembler::get_unsigned_2_byte_index_at_bcp(Register reg,int bcp_offset) {
  assert(bcp_offset >= 0, "bcp is still pointing to start of bytecode");
  load_unsigned_short(reg, Address(r13, bcp_offset));
  bswapl(reg);
  shrl(reg, 16);
}

生成的汇编代码如下:

// %r13保存当前解释器的字节码指令地址,将此地址偏移1个字节后获取2个字节的内容并加载到%edx中
0x00007fffe1022b10: movzwl 0x1(%r13),%edx  
// bswap会让32位寄存器%edx中存储的内容进行字节次序反转
0x00007fffe1022b15: bswap  %edx
// shr会将%edx中的内容右移16位
0x00007fffe1022b17: shr    $0x10,%edx

调用get_cpool_and_tags()方法获取常量池首地址放入rcx寄存器,获取常量池中元素类型数组_tags首地址,放入rax中,方法的实现如下:

void get_cpool_and_tags(Register cpool, Register tags) {
    get_constant_pool(cpool);
    movptr(tags, Address(cpool, ConstantPool::tags_offset_in_bytes()));
}

void get_constant_pool(Register reg) {
    get_const(reg);
    movptr(reg, Address(reg, ConstMethod::constants_offset()));
}

void get_const(Register reg) {
    get_method(reg);
    movptr(reg, Address(reg, Method::const_offset()));
}

void get_method(Register reg) {
    movptr(reg, Address(rbp, frame::interpreter_frame_method_offset * wordSize));
}

生成的汇编如下:

// %rbp-0x18后指向Method*,存储到%rsi中
0x00007fffe1022b1a: mov    -0x18(%rbp),%rsi 
// %rsi偏移0x10后就是ConstMethod*,存储到%rsi中
0x00007fffe1022b1e: mov    0x10(%rsi),%rsi 
// %rsi偏移0x8后就是ConstantPool*,存储到%rsi中
0x00007fffe1022b22: mov    0x8(%rsi),%rsi  
// %rsi偏移0x10后就是tags属性的地址,存储到%rax中
0x00007fffe1022b26: mov    0x10(%rsi),%rax

回到TemplateTable::_new()方法,继续执行如下代码:

// 判断_tags数组中对应元素类型是否为JVM_CONSTANT_Class,不是则跳往slow_case处
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);

// get InstanceKlass
// 获取创建对象所属类地址,放入rcx中,即类的运行时数据结构InstanceKlass,并将其入栈
__ movptr(rsi, Address(rsi, rdx,Address::times_8, sizeof(ConstantPool)));

// make sure klass is initialized & doesn't have finalizer
// make sure klass is fully initialized
//  判断类是否已经被初始化过,没有初始化过的话直接跳往slow_close进行慢速分配,
//  如果对象所属类已经被初始化过,则会进入快速分配
__ cmpb(
        Address(rsi,InstanceKlass::init_state_offset()),
        InstanceKlass::fully_initialized);
__ jcc(Assembler::notEqual, slow_case);

// get instance_size in InstanceKlass (scaled to a count of bytes)
// 此时rcx寄存器中存放的是类InstanceKlass的内存地址,利用偏移获取类对象大小并存入rdx寄存器
__ movl( rdx,
         Address(rsi,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);

生成的汇编代码如下:

// %rax中存储的是_tags数组的首地址<br>// %rdx中存储的就是new指令后操作数,既常量池索引
// 判断常量池索引处的类型是否为JVM_CONSTANT_Class
0x00007fffe1022b2a: cmpb   $0x7,0x4(%rax,%rdx,1)
// 不是则跳往slow_case处
0x00007fffe1022b2f: jne    0x00007fffe1022b35<br>
// %rsi中存储的是常量池首地址
// %rdx中存储的是new指令后的操作数,即常量池索引
// 获取要创建对象所属的类地址,即InstanceKlass地址,放入%rsi中
0x00007fffe1022b35: mov    0x58(%rsi,%rdx,8),%rsi<br>
// 判断类是否已经被初始化,没有初始化就跳转到slow_case处执行慢速分配
0x00007fffe1022b3a: cmpb   $0x4,0x16a(%rsi)
0x00007fffe1022b41: jne    0x00007fffe1022b47<br>// 当执行如下代码时,表示类已经被初始化过
// %rsi中存放的是InstanceKlass地址,利用偏移获取此类创建的对象大小(也就是Java类创建的Java对象的大小),存入%edx中
0x00007fffe1022b47: mov    0xc(%rsi),%edx
// 判断一下类是否有finalize()方法,如果有,跳往slow_case处执行慢速分配 
0x00007fffe1022b4a: test $0x1,%edx 
0x00007fffe1022b50: jne 0x00007fffe1022b56

当计算出了创建对象的大小后就可以执行内存分配了,回到TemplateTable::_new()方法,继续执行如下代码: 

if (UseTLAB) { // 默认UseTLAB的值为true
    // 获取TLAB区剩余空间首地址,放入%rax中
    __ movptr(rax, Address(r15_thread, in_bytes(JavaThread::tlab_top_offset())));

    // %rdx保存对象大小,根据TLAB空闲区首地址可计算出对象分配后的尾地址,然后放入%rbx中
    __ lea(rbx, Address(rax, rdx, Address::times_1));

    // 将%rbx中对象尾地址与TLAB空闲区尾地址进行比较
    __ cmpptr(rbx, Address(r15_thread, in_bytes(JavaThread::tlab_end_offset())));

    // 如果%rbx大小TLAB空闲区结束地址,表明TLAB区空闲区大小不足以分配该对象,
    // 在allow_shared_alloc(允许在Eden区分配)情况下,跳转到allocate_shared,否则跳转到slow_case处
    __ jcc(Assembler::above, allow_shared_alloc ? allocate_shared : slow_case);

    // 执行到这里,说明TLAB区有足够的空间分配对象
    // 对象分配后,更新TLAB空闲区首地址为分配对象后的尾地址
    __ movptr(Address(r15_thread, in_bytes(JavaThread::tlab_top_offset())), rbx);

    // 如果TLAB区默认会对回收的空闲区清零,那么就不需要在为对象变量进行清零操作了,
    // 直接跳往对象头初始化处运行
    if (ZeroTLAB) {
      // the fields have been already cleared
      __ jmp(initialize_header);
    } else {
      // initialize both the header and fields
      __ jmp(initialize_object);
    }
}

其中allocate_shared变量值的计算如下:

const bool allow_shared_alloc = Universe::heap()->supports_inline_contig_alloc() && !CMSIncrementalMode;

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

生成的汇编代码如下:

// 获取TLAB区剩余空间首地址,放入%rax
0x00007fffe1022b56: mov    0x70(%r15),%rax
// %rdx已经记录了对象大小,根据TLAB空闲区首地址计算出对象分配后的尾地址,放入rbx中
0x00007fffe1022b5a: lea    (%rax,%rdx,1),%rbx
// 将rbx中内容与TLAB空闲区尾地址进行比较
0x00007fffe1022b5e: cmp    0x80(%r15),%rbx
// 如果比较结果表明rbx > TLAB空闲区尾地址,则表明TLAB区空闲区大小不足以分配该对象,
// 在allow_shared_alloc(允许在Eden区分配)情况下,就直接跳往Eden区分配内存标号处运行
0x00007fffe1022b65: ja     0x00007fffe1022b6b
// 因为对象分配后,TLAB区空间变小,所以需要更新TLAB空闲区首地址为分配对象后的尾地址
0x00007fffe1022b6b: mov    %rbx,0x70(%r15)
// TLAB区默认不会对回收的空闲区清零,跳往initialize_object
0x00007fffe1022b6f: jmpq 0x00007fffe1022b74

如果在TLAB区分配失败,会直接在Eden区进行分配,回到TemplateTable::_new()方法继续执行如下代码:  

// Allocation in the shared Eden, if allowed.
// rdx: instance size in bytes
if (allow_shared_alloc) { 
    // TLAB区分配失败会跳到这里
    __ bind(allocate_shared);

    // 获取Eden区剩余空间的首地址和结束地址
    ExternalAddress top((address)Universe::heap()->top_addr());
    ExternalAddress end((address)Universe::heap()->end_addr());

    const Register RtopAddr = rscratch1;
    const Register RendAddr = rscratch2;

    __ lea(RtopAddr, top);
    __ lea(RendAddr, end);
    // 将Eden空闲区首地址放入rax寄存器中
    __ movptr(rax, Address(RtopAddr, 0));

    // For retries rax gets set by cmpxchgq
    Label retry;
    __ bind(retry);
    // 计算对象尾地址,与空闲区尾地址进行比较,内存不足则跳往慢速分配。
    __ lea(rbx, Address(rax, rdx, Address::times_1));
    __ cmpptr(rbx, Address(RendAddr, 0));
    __ 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 rax此时记录了对象分配的内存首地址
    // rbx: object end   rbx此时记录了对象分配的内存尾地址
    // rdx: instance size in bytes  rdx记录了对象大小
    if (os::is_MP()) {
      __ lock();
    }
    // 利用CAS操作,更新Eden空闲区首地址为对象尾地址,因为Eden区是线程共用的,所以需要加锁。
    __ cmpxchgptr(rbx, Address(RtopAddr, 0));

    // if someone beat us on the allocation, try again, otherwise continue
    __ jcc(Assembler::notEqual, retry);

    __ incr_allocated_bytes(r15_thread, rdx, 0);
}

生成的汇编代码如下:

-- allocate_shared --
// 获取Eden区剩余空间的首地址和结束地址并分别存储到%r10和%r11中
0x00007fffe1022b74: movabs $0x7ffff0020580,%r10
0x00007fffe1022b7e: movabs $0x7ffff0020558,%r11
// 将Eden空闲区首地址放入%rax
0x00007fffe1022b88: mov    (%r10),%rax
// 计算对象尾地址,与Eden空闲区结束地址进行比较,内存不足则跳往慢速分配slow_case
0x00007fffe1022b8b: lea    (%rax,%rdx,1),%rbx
0x00007fffe1022b8f: cmp    (%r11),%rbx
0x00007fffe1022b92: ja     0x00007fffe1022b98
// 利用CAS操作,更新Eden空闲区首地址为对象尾地址,因为Eden区是线程共用的,所以需要加锁
0x00007fffe1022b98: lock   cmpxchg %rbx,(%r10)
0x00007fffe1022b9d: jne    0x00007fffe1022b8b
0x00007fffe1022b9f: add    %rdx,0xd0(%r15)

回到TemplateTable::_new()方法,对象所需内存已经分配好后,就会进行对象的初始化了,先初始化对象实例数据。继续执行如下代码:

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);
    // 如果rdx和sizeof(oopDesc)大小一样,即对象所需大小和对象头大小一样,
    // 则表明对象真正的实例数据内存为0,不需要进行对象实例数据的初始化,
    // 直接跳往对象头初始化处即可。Hotspot中虽然对象头在内存中排在对象实例数据前,
    // 但是会先初始化对象实例数据,再初始化对象头。
    __ decrementl(rdx, sizeof(oopDesc));
    __ jcc(Assembler::zero, initialize_header);

    // Initialize object fields
    // 执行异或,使得rcx为0,为之后给对象变量赋零值做准备
    __ xorl(rcx, rcx); // use zero reg to clear memory (shorter code)
    __ shrl(rdx, LogBytesPerLong);  // divide by oopSize to simplify the loop
    {
      // 此处以rdx(对象大小)递减,按字节进行循环遍历对内存,初始化对象实例内存为零值
      // rax中保存的是对象的首地址
      Label loop;
      __ bind(loop);
      __ movq(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - oopSize ), rcx);
      __ decrementl(rdx);
      __ jcc(Assembler::notZero, loop);
    }

    // initialize object header only.
    // 对象实例数据初始化好后,开始初始化对象头(就是初始化oop中的mark和metadata属性的初始化)
    __ bind(initialize_header);
    // 是否使用偏向锁,大多时一个对象只会被同一个线程访问,所以在对象头中记录获取锁的线程id,
    // 下次线程获取锁时就不需要加锁了。
    if (UseBiasedLocking) {
      // 将类的偏向锁相关数据移动到对象头部
      // rax中保存的是对象的首地址
      __ movptr(rscratch1, Address(rsi, Klass::prototype_header_offset()));
      __ movptr(Address(rax, oopDesc::mark_offset_in_bytes()), rscratch1);
    } else {
      __ movptr(Address(rax, oopDesc::mark_offset_in_bytes()),
               (intptr_t) markOopDesc::prototype()); // header (address 0x1)
    }
    // 此时rcx保存了InstanceKlass,rax保存了对象首地址,此处保存对象所属的类数据InstanceKlass放入对象头中,
    // 对象oop中的_metadata属性存储对象所属的类InstanceKlass的指针
    __ xorl(rcx, rcx);             // use zero reg to clear memory (shorter code)
    __ store_klass_gap(rax, rcx);  // zero klass gap for compressed oops
    __ store_klass(rax, rsi);      // store klass last

    // ...
    __ jmp(done);
}

为虚拟机添加参数 -XX:-UseCompressedOops,表示不进行指针压缩,则生成的汇编代码如下:

-- initialize_object --
// %edx减去对象头大小0x10后,将结果存储到%edx
0x00007fffe1022ba6: sub    $0x10,%edx
// 如果%edx等于0,则跳转到initialize_header
0x00007fffe1022ba9: je     0x00007fffe1022bbd
// 执行异或,使得%ecx为0,为之后给对象变量赋零值做准备
0x00007fffe1022baf: xor    %ecx,%ecx
0x00007fffe1022bb1: shr    $0x3,%edx

-- loop --
// 此处以%rdx(对象大小)递减,按字节进行循环遍历对内存,初始化对象实例内存为零值
// %rax中保存的是对象首地址
0x00007fffe1022bb4: mov    %rcx,0x8(%rax,%rdx,8)
0x00007fffe1022bb9: dec    %edx
// 如果不相等,跳转到loop
0x00007fffe1022bbb: jne    0x00007fffe1022bb4

-- initialize_header --
// 对象实例数据初始化好后,就开始初始化对象头
// 是否使用偏向锁,大多时一个对象只会被同一个线程访问,所以在对象头中记录获取锁的线程id,
// 下次线程获取锁时就不需要加锁了
0x00007fffe1022bbd: mov    0xb0(%rsi),%r10
0x00007fffe1022bc4: mov    %r10,(%rax)
// rax保存了对象首地址,
0x00007fffe1022bc7: xor    %ecx,%ecx
// %rsi中保存的就是InstanceKlass对象的地址,%rax保存了对象首地址,偏移0x08后就是metadata
// 将InstanceKlass对象保存到对象oop中的_metadata属性中
0x00007fffe1022bc9: mov    %rsi,0x8(%rax)
// ...
// 跳转到done处执行
0x00007fffe1022c02: jmpq   0x00007fffe1022c07

调用的store_klass_gap()函数的实现如下:

void MacroAssembler::store_klass_gap(Register dst, Register src) {
  if (UseCompressedClassPointers) {
    // Store to klass gap in destination
    movl(Address(dst, oopDesc::klass_gap_offset_in_bytes()), src);
  }
}

调用的函数的实现如下:

void MacroAssembler::store_klass(Register dst, Register src) {
#ifdef _LP64
  if (UseCompressedClassPointers) {
    encode_klass_not_null(src);
    movl(Address(dst, oopDesc::klass_offset_in_bytes()), src);
  } else
#endif
    movptr(Address(dst, oopDesc::klass_offset_in_bytes()), src);
}

UseCompressedClassPointers在设置了-XX:-UseCompressedOops命令后值都为false。

回到TemplateTable::_new()方法,继续执行如下代码:

// 慢速分配,如果类没有被初始化过,会跳到此处执行
__ bind(slow_case);
// 获取常量池首地址,存入rarg1寄存器
__ get_constant_pool(c_rarg1);
// 获取new指令后操作数,即类在常量池中的索引,存入rarg2寄存器
__ get_unsigned_2_byte_index_at_bcp(c_rarg2, 1);

// 调用InterpreterRuntime::_new()函数进行对象内存分配
call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), c_rarg1, c_rarg2);

__ bind(done);

生成的汇编代码如下:

-- slow_case --
// 慢速分配,如果类没有被初始化过,会跳到此处执行
// 获取常量池地址并保存到%rsi中
0x00007fffe1022c07: mov    -0x18(%rbp),%rsi
0x00007fffe1022c0b: mov    0x10(%rsi),%rsi
0x00007fffe1022c0f: mov    0x8(%rsi),%rsi

// 获取new指令后操作数,即类在常量池中的索引,存入%edx中
0x00007fffe1022c13: movzwl 0x1(%r13),%edx
0x00007fffe1022c18: bswap  %edx
0x00007fffe1022c1a: shr    $0x10,%edx

// 如下的汇编代码调用了InterpreterRuntime::_new()函数,不过在调用函数前后,需要进行一些准备,如
// 为调用的函数准备参数等工作,后面在介绍方法执行引擎时会详细分析
0x00007fffe1022c1d: callq  0x00007fffe1022c27
0x00007fffe1022c22: jmpq   0x00007fffe1022cba
0x00007fffe1022c27: lea    0x8(%rsp),%rax
0x00007fffe1022c2c: mov    %r13,-0x38(%rbp)
0x00007fffe1022c30: mov    %r15,%rdi
0x00007fffe1022c33: mov    %rbp,0x200(%r15)
0x00007fffe1022c3a: mov    %rax,0x1f0(%r15)
0x00007fffe1022c41: test   $0xf,%esp
0x00007fffe1022c47: je     0x00007fffe1022c5f
0x00007fffe1022c4d: sub    $0x8,%rsp
0x00007fffe1022c51: callq  0x00007ffff66b302e
0x00007fffe1022c56: add    $0x8,%rsp
0x00007fffe1022c5a: jmpq   0x00007fffe1022c64
0x00007fffe1022c5f: callq  0x00007ffff66b302e
0x00007fffe1022c64: movabs $0x0,%r10
0x00007fffe1022c6e: mov    %r10,0x1f0(%r15)
0x00007fffe1022c75: movabs $0x0,%r10
0x00007fffe1022c7f: mov    %r10,0x200(%r15)
0x00007fffe1022c86: cmpq   $0x0,0x8(%r15)
0x00007fffe1022c8e: je     0x00007fffe1022c99
0x00007fffe1022c94: jmpq   0x00007fffe1000420
0x00007fffe1022c99: mov    0x250(%r15),%rax
0x00007fffe1022ca0: movabs $0x0,%r10
0x00007fffe1022caa: mov    %r10,0x250(%r15)
0x00007fffe1022cb1: mov    -0x38(%rbp),%r13
0x00007fffe1022cb5: mov    -0x30(%rbp),%r14
0x00007fffe1022cb9: retq   

-- done --

在汇编代码中调用的InterpreterRuntime::_new()函数的实现如下:

源代码位置:share/vm/interpreter/inpterpreterRuntime.cpp

IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
  Klass* k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);

  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);

  // Make sure klass is initialized
  klass->initialize(CHECK);

  // At this point the class may not be fully initialized
  // because of recursive initialization. If it is fully
  // initialized & has_finalized is not set, we rewrite
  // it into its fast version (Note: no locking is needed
  // here since this is an atomic byte write and can be
  // done more than once).
  //
  // Note: In case of classes with has_finalized we don't
  //       rewrite since that saves us an extra check in
  //       the fast version which then would call the
  //       slow version anyway (and do a call back into
  //       Java).
  //       If we have a breakpoint, then we don't rewrite
  //       because the _breakpoint bytecode would be lost.
  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END

如上方法进行类的加载和对象分配,并将分配的对象地址返回,存入rax寄存器中。调用的klass->allocate_instance()方法的实现如下:

instanceOop instanceKlass::allocate_instance(TRAPS) {
  assert(!oop_is_instanceMirror(), "wrong allocation path");
  //是否重写finalize()方法
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  //分配的对象的大小
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, as_klassOop());

  instanceOop i;

  //分配对象
  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

调用size_helper()方法获取实例对象需要的空间大小。然后调用CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS)来为对象分配内存。此方法还需要判断类是否重写了finalize()方法,重写finalize()方法的类会让实例对象会加入finalize队列,队列里面的对象在GC前会调用finalize()方法。

调用的obj_allocate()方法的实现涉及到的逻辑比较多,这里暂不介绍,后面在介绍垃圾回收时会详细介绍。  
总结一下如上的内存分配大概流程:

(1)首先在TLAB区分配;

(2)如果在TLAB区分配失败,则在Eden区分配;

(3)如果无法在TLAB和Eden区分配,那么会调用InterpreterRuntime::_new()函数进行分配。

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关参数的设置。后面我们在介绍具体的垃圾收集器时再细化一下这个对象分配的过程。

正文到此结束