對象的創建
- 2020 年 8 月 15 日
- 筆記
Java對象創建的流程大概如下:
- 檢查對象所屬類是否已經被載入解析;
- 為對象分配記憶體空間;
- 將分配給對象的記憶體初始化為零值;
- 執行對象的<init>方法進行初始化。
舉個例子如下:
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數組的首地址
// %rdx中存儲的就是new指令後操作數,既常量池索引 // 判斷常量池索引處的類型是否為JVM_CONSTANT_Class 0x00007fffe1022b2a: cmpb $0x7,0x4(%rax,%rdx,1) // 不是則跳往slow_case處 0x00007fffe1022b2f: jne 0x00007fffe1022b35
// %rsi中存儲的是常量池首地址 // %rdx中存儲的是new指令後的操作數,即常量池索引 // 獲取要創建對象所屬的類地址,即InstanceKlass地址,放入%rsi中 0x00007fffe1022b35: mov 0x58(%rsi,%rdx,8),%rsi
// 判斷類是否已經被初始化,沒有初始化就跳轉到slow_case處執行慢速分配 0x00007fffe1022b3a: cmpb $0x4,0x16a(%rsi) 0x00007fffe1022b41: jne 0x00007fffe1022b47
// 當執行如下程式碼時,表示類已經被初始化過 // %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()方法。
總結一下如上的記憶體分配大概流程:
(1)首先在TLAB區分配;
(2)如果在TLAB區分配失敗,則在Eden區分配;
(3)如果無法在TLAB和Eden區分配,那麼會調用InterpreterRuntime::_new()函數進行分配。
對象的記憶體分配,往大方向講,就是在堆上分配,對象主要分配在新生代的Eden區上。少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與記憶體相關參數的設置。後面我們在介紹具體的垃圾收集器時再細化一下這個對象分配的過程。
相關文章的鏈接如下:
1、在Ubuntu 16.04上編譯OpenJDK8的源程式碼
13、類載入器
14、類的雙親委派機制
15、核心類的預裝載
16、Java主類的裝載
17、觸發類的裝載
18、類文件介紹
19、文件流
20、解析Class文件
21、常量池解析(1)
22、常量池解析(2)
23、欄位解析(1)
24、欄位解析之偽共享(2)
25、欄位解析(3)
28、方法解析
29、klassVtable與klassItable類的介紹
30、計算vtable的大小
31、計算itable的大小
32、解析Class文件之創建InstanceKlass對象
33、欄位解析之欄位注入
34、類的連接
35、類的連接之驗證
36、類的連接之重寫(1)
37、類的連接之重寫(2)
38、方法的連接
39、初始化vtable
40、初始化itable
41、類的初始化
作者持續維護的個人部落格 classloading.com。
關注公眾號,有HotSpot源碼剖析系列文章!
參考文章:
(1)new背後的故事
(3)對象創建
(7)RednaxelaFX、你假笨關於TLAB的一些分析總結
(8)JVM之創建對象源碼分析