JVM之創(chuàng)建對象源碼分析

之前對Java對象的創(chuàng)建一直都是概念上的了解,沒有在源碼層面進(jìn)行過分析,這段時間在看HotSpot,就順便了解了下JVM究竟是如何創(chuàng)建Java對象的。

一:Java對象創(chuàng)建流程

  1. 檢查對象所屬類是否已經(jīng)被加載解析;
  2. 為對象分配內(nèi)存空間;
  3. 將分配給對象的內(nèi)存初始化為零值;
  4. 執(zhí)行對象的<init>方法,用來初始化對象。

我之前收藏過一張圖,忘記出處了,此處引用下:

二:對象創(chuàng)建源碼分析

2.1:了解對象創(chuàng)建指令

我們先從一個簡單的Demo入手:

public class Test {

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

    static class Dog{
        int age;
    }
}

上面代碼很簡單,在main()中創(chuàng)建了一個Dog對象,寫這個Demo是為了讓我們看看new Dog()在編譯成字節(jié)碼后會變成什么。

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
}

可見new Dog()變成了new #2,new 是java眾多字節(jié)碼中用來實例化對象的字節(jié)碼,不用我說大家肯定也清楚,關(guān)鍵后面的 #2 是個啥?

類在編譯成字節(jié)碼時,會生成類所屬的常量池,常量池中記錄了各種符號引用及常量,#2 其實就是常量池中索引為2的常量項,此處指向的是Dog類的符號。

2.2:new指令源碼分析

上面已經(jīng)對類創(chuàng)建的字節(jié)碼進(jìn)行了簡單介紹,我們已經(jīng)知道了用于對象創(chuàng)建的字節(jié)碼指令為new,接下來就可以對new指令進(jìn)行源碼分析了。

在我上一篇博客中對java字節(jié)碼指令的運行進(jìn)行了介紹,主要講的是模板解釋器,今天我們?nèi)匀粚δ0褰忉屍髦衝ew指令的運行進(jìn)行講解,不清楚模板解釋器的讀者可以看看《JVM之模板解釋器》

我們先來看看new指令對應(yīng)的匯編代碼,代碼很長,我們稍后會進(jìn)行逐步分析,不想直接看代碼的同學(xué)可以先跳過。

/hotspot/src/cpu/x86/vm/templateTable_x86.cpp

void TemplateTable::_new() {
  transition(vtos, atos);
  __ get_unsigned_2_byte_index_at_bcp(rdx, 1);
  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(rcx, rax);

  // 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(rcx);  // 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(rcx);
#ifndef _LP64
  if (UseTLAB || allow_shared_alloc) {
    __ get_thread(thread);
  }
#endif // _LP64

  if (UseTLAB) {
    __ 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())), rbx);
    if (ZeroTLAB) {
      // 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_top((address)Universe::heap()->top_addr());
    ExternalAddress heap_end((address)Universe::heap()->end_addr());

    Label retry;
    __ bind(retry);
    __ 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(oopDesc));
    __ jcc(Assembler::zero, initialize_header);

    // Initialize topmost object field, divide rdx by 8, check if odd and
    // test if zero.
    __ xorl(rcx, rcx);    // 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(L);
    // rdx must be > 0, no extra check needed here
#endif

    // initialize remaining object fields: rdx was a multiple of 8
    { Label loop;
    __ bind(loop);
    __ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 1*oopSize), rcx);
    NOT_LP64(__ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 2*oopSize), rcx));
    __ decrement(rdx);
    __ jcc(Assembler::notZero, loop);
    }

    // initialize object header only.
    __ bind(initialize_header);
    if (UseBiasedLocking) {
      __ pop(rcx);   // get saved klass back in the register.
      __ movptr(rbx, Address(rcx, Klass::prototype_header_offset()));
      __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ()), rbx);
    } else {
      __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ()),
                (intptr_t)markOopDesc::prototype()); // header
      __ pop(rcx);   // get saved klass back in the register.
    }
#ifdef _LP64
    __ xorl(rsi, rsi); // use zero reg to clear memory (shorter code)
    __ store_klass_gap(rax, rsi);  // zero klass gap for compressed oops
#endif
    __ store_klass(rax, rcx);  // klass

    {
      SkipIfEqual skip_if(_masm, &DTraceAllocProbes, 0);
      // Trigger dtrace event for fastpath
      __ push(atos);
      __ call_VM_leaf(
           CAST_FROM_FN_PTR(address, SharedRuntime::dtrace_object_alloc), rax);
      __ pop(atos);
    }

    __ jmp(done);
  }

  // slow case
  __ bind(slow_case);
  __ pop(rcx);   // restore stack pointer to what it was when we came in.
  __ bind(slow_case_no_pop);

  Register rarg1 = LP64_ONLY(c_rarg1) NOT_LP64(rax);
  Register rarg2 = LP64_ONLY(c_rarg2) NOT_LP64(rdx);

  __ get_constant_pool(rarg1);
  __ get_unsigned_2_byte_index_at_bcp(rarg2, 1);
  call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
   __ verify_oop(rax);

  // continue
  __ bind(done);
}

下面我們來逐步看看上面這些代碼主要干了啥。
1:獲取new指令后的操作數(shù),即類在常量池的索引,放入rdx寄存器中。bcp即rsi寄存器,用來記錄當(dāng)前解釋器運行的字節(jié)碼指令地址,類似SS:IP寄存器,用來進(jìn)行pc計數(shù)。這個方法主要就是獲取當(dāng)前運行指令地址偏移一個字節(jié)處內(nèi)容。

__ get_unsigned_2_byte_index_at_bcp(rdx, 1);

2:獲取常量池首地址放入rcx寄存器,獲取常量池中元素類型數(shù)組_tags首地址,放入rax中。_tags數(shù)組按順序存放了每個常量池元素的類型。

__ get_cpool_and_tags(rcx, rax);

3:判斷_tags數(shù)組中對應(yīng)元素類型是否為JVM_CONSTANT_Class,不是則跳往slow_case_no_pop處。

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);

4:獲取創(chuàng)建對象所屬類地址,放入rcx中,即類的運行時數(shù)據(jù)結(jié)構(gòu)InstanceKlass,并將其入棧。

__ movptr(rcx, Address(rcx, rdx, Address::times_ptr, sizeof(ConstantPool)));
__ push(rcx);  // save the contexts of klass for initializing the header

5:判斷類是否已經(jīng)被解析過,沒有解析的話直接跳往slow_close,slow_case即慢速分配,如果對象所屬類已經(jīng)被解析過,則會進(jìn)入快速分配,否則會進(jìn)入慢速分配,去進(jìn)行類的解析。

__ cmpb(Address(rcx, InstanceKlass::init_state_offset()), InstanceKlass::fully_initialized);
__ jcc(Assembler::notEqual, slow_case);

6:此時rcx中存放的是類InstanceKlass的內(nèi)存地址,利用偏移獲取類實例大小,存入rdx寄存器,對象的大小早在類加載時就已經(jīng)確定了。

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

7:嘗試在TLAB區(qū)為對象分配內(nèi)存,TLAB即ThreadLocalAllocationBuffers(線程局部分配緩存)。每個線程都有自己的一塊內(nèi)存區(qū)域,用于分配對象,這塊內(nèi)存區(qū)域便為TLAB區(qū)。這樣的好處是在分配內(nèi)存時,無需對一整塊內(nèi)存進(jìn)行加鎖。TLAB只是在分配對象時的操作屬于線程私有,分配的對象對于其他線程仍是可讀的。

if (UseTLAB) {
    // 獲取TLAB區(qū)剩余空間首地址,放入rax寄存器。
    __ movptr(rax, Address(thread, in_bytes(JavaThread::tlab_top_offset())));
    // rdx寄存器已經(jīng)記錄了對象大小,此處及根據(jù)TLAB空閑區(qū)首地址,計算出對象分配后,對象尾地址,放入rbx中
    __ lea(rbx, Address(rax, rdx, Address::times_1));
    // 將rbx中內(nèi)容與TLAB空閑區(qū)尾地址進(jìn)行比較。
    __ cmpptr(rbx, Address(thread, in_bytes(JavaThread::tlab_end_offset())));
    // 如果上面比較結(jié)果表明rbx > TLAB空閑區(qū)尾地址,則表明TLAB區(qū)空閑區(qū)大小不足以分配該對象,那么在allow_shared_alloc(允許在Eden區(qū)分配)情況下,就直接跳往Eden區(qū)分配內(nèi)存標(biāo)號處運行,即第8步
    __ jcc(Assembler::above, allow_shared_alloc ? allocate_shared : slow_case);
   // 因為對象分配后,TLAB區(qū)空間變小,此處更新TLAB空閑區(qū)首地址為對象尾地址
    __ movptr(Address(thread, in_bytes(JavaThread::tlab_top_offset())), rbx);
   // 如果TLAB區(qū)默認(rèn)會對回收的空閑區(qū)清零,那么就不需要在為對象變量進(jìn)行清零操作了,直接跳往對象頭初始化處運行。有同學(xué)可能會問為什么要進(jìn)行清零操作呢?因為分配的內(nèi)存可能還保留著上次分配給其他對象時的數(shù)據(jù),內(nèi)存塊雖然被回收了,但是之前的數(shù)據(jù)沒有被清除,會污染新對象。
   if (ZeroTLAB) {
      // the fields have been already cleared
      __ jmp(initialize_header);
   } else {
      // initialize both the header and fields
      __ jmp(initialize_object);
   }
}

8:如果在TLAB區(qū)分配失敗,會直接在Eden區(qū)進(jìn)行分配,具體過程和第7步很像。

if (allow_shared_alloc) {
    // TLAB區(qū)分配失敗會跳到這。
    __ bind(allocate_shared);
    // 獲取Eden區(qū)剩余空間的首地址和結(jié)束地址。
    ExternalAddress heap_top((address)Universe::heap()->top_addr());
    ExternalAddress heap_end((address)Universe::heap()->end_addr());

    Label retry;
    __ bind(retry);

    // 將空閑區(qū)首地址放入rax中,用作對象分配開始處。
    __ movptr(rax, heap_top);

    // 計算對象尾地址,與空閑區(qū)尾地址進(jìn)行比較,內(nèi)存不足則跳往慢速分配。
    __ lea(rbx, Address(rax, rdx, Address::times_1));
    __ cmpptr(rbx, heap_end);
    __ jcc(Assembler::above, slow_case);

    // rax,: object begin,rax此時記錄了對象分配的內(nèi)存首地址
    // rbx,: object end    rbx此時記錄了對象分配的內(nèi)存尾地址
    // rdx: instance size in bytes rdx記錄了對象大小

    // 利用CAS操作,更新Eden空閑區(qū)首地址為對象尾地址,因為Eden區(qū)是線程共用的,所以需要加鎖。
    __ 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);
}

9:對象所需內(nèi)存已經(jīng)分配好后,就會進(jìn)行對象的初始化了,先初始化對象實例數(shù)據(jù)。

// 開始初始化對象處
__ bind(initialize_object);
// 如果rdx和sizeof(oopDesc)大小一樣,即對象所需大小和對象頭大小一樣,則表明對象真正的實例數(shù)據(jù)內(nèi)存為0,那么就不需要進(jìn)行對象實例數(shù)據(jù)的初始化了,直接跳往對象頭初始化處即可。Hotspot中雖然對象頭在內(nèi)存中排在對象實例數(shù)據(jù)前,但是會先初始化對象實例數(shù)據(jù),再初始化對象頭。
__ decrement(rdx, sizeof(oopDesc));
__ jcc(Assembler::zero, initialize_header);

// 執(zhí)行異或,使得rcx為0,為之后給對象變量賦零值做準(zhǔn)備
__ xorl(rcx, rcx);    // use zero reg to clear memory (shorter code)
__ shrl(rdx, LogBytesPerLong); // divide by 2*oopSize and set carry flag if odd

Label L;
__ jccb(Assembler::carryClear, L);
__ stop("object size is not multiple of 2 - adjust this code");
__ bind(L);

// 此處以rdx(對象大?。┻f減,按字節(jié)進(jìn)行循環(huán)遍歷對內(nèi)存,初始化對象實例內(nèi)存為零值。
{ Label loop;
__ bind(loop);
__ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 1*oopSize), rcx);
NOT_LP64(__ movptr(Address(rax, rdx, Address::times_8, sizeof(oopDesc) - 2*oopSize), rcx));
__ decrement(rdx);
__ jcc(Assembler::notZero, loop);
}

10:對象實例數(shù)據(jù)初始化好后,就開始進(jìn)行對象頭的初始化了。

// 初始化對象頭標(biāo)號處
__ bind(initialize_header);

// 是否使用偏向鎖,大多時一個對象只會被同一個線程訪問,所以在對象頭中記錄獲取鎖的線程id,下次線程獲取鎖時就不需要加鎖了。
if (UseBiasedLocking) {
    // 第4步中有將類數(shù)據(jù)InstanceKlass的地址入棧,此時重新出棧,放入rcx寄存器。
    __ pop(rcx);  
    // 接下來兩步將類的偏向鎖相關(guān)數(shù)據(jù)移動到對象頭部
    __ movptr(rbx, Address(rcx, Klass::prototype_header_offset()));
    __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ()), rbx);
} else {
    __ movptr(Address(rax, oopDesc::mark_offset_in_bytes ()),
            (intptr_t)markOopDesc::prototype()); // header
    __ pop(rcx);   // get saved klass back in the register.
}
// 此時rcx保存了InstanceKlass,rax保存了對象首地址,此處保存對象所屬的類數(shù)據(jù)InstanceKlass放入對象頭中,對象頭尾oopDesc類型,里面有個_metadata聯(lián)合體,_metadata中專門有個Klass指針用來指向類所屬對象,此處其實就是將InstanceKlass地址放入該指針中。
__ store_klass(rax, rcx);  // klass

{
  SkipIfEqual skip_if(_masm, &DTraceAllocProbes, 0);
  // Trigger dtrace event for fastpath
  __ push(atos);
  __ call_VM_leaf(
       CAST_FROM_FN_PTR(address, SharedRuntime::dtrace_object_alloc), rax);
  __ pop(atos);
}

__ jmp(done);

11:慢速分配,經(jīng)過上面分析可知,如果類沒有被加載解析,會跳到此處執(zhí)行。

 __ bind(slow_case);
 // 因為第4步有將InsanceKlass入棧,這里用不上,重新出棧,還原棧頂數(shù)據(jù)。
 __ pop(rcx);   // restore stack pointer to what it was when we came in.
 __ bind(slow_case_no_pop);

 Register rarg1 = LP64_ONLY(c_rarg1) NOT_LP64(rax);
 Register rarg2 = LP64_ONLY(c_rarg2) NOT_LP64(rdx);

 // 獲取常量池地址,存入rarg1寄存器。
 __ get_constant_pool(rarg1);
 // 獲取new 指令后操作數(shù),即類在常量池中的索引,放入rarg2寄存器。
 __ get_unsigned_2_byte_index_at_bcp(rarg2, 1);
 // 進(jìn)入InterpreterRuntime::_new生成的機(jī)器指令地址處,開始執(zhí)行,里面會進(jìn)行類的加載和對象分配,并將分配的對象地址返回,存入rax寄存器中。
 call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
 __ verify_oop(rax);

 // 創(chuàng)建結(jié)束
 __ bind(done);

三:總結(jié)

對象的創(chuàng)建到這就結(jié)束了,希望大家能對java對象創(chuàng)建有了更多的了解。因為上面拿模板解釋器進(jìn)行講解的,都是匯編語言,其實大家也可以直接看看字節(jié)碼解釋器中對象創(chuàng)建的方法,比較好理解。本人能力有限,如有錯誤,請多指正。目前正在研讀HotSpot源碼,如果有同學(xué)比較感興趣,也可以一起交流,附上微信:wang_atbeijing

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容