之前對Java對象的創(chuàng)建一直都是概念上的了解,沒有在源碼層面進(jìn)行過分析,這段時間在看HotSpot,就順便了解了下JVM究竟是如何創(chuàng)建Java對象的。
一:Java對象創(chuàng)建流程
- 檢查對象所屬類是否已經(jīng)被加載解析;
- 為對象分配內(nèi)存空間;
- 將分配給對象的內(nèi)存初始化為零值;
- 執(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