對象的創建(不包括數組和Class對象)
虛擬機遇到一條new指令時,首相檢查這個指令的參數是否能再常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
類加載檢查通過后,虛擬機將為新生對象分配內存。對象所需的大小再類加載完成后便可完全確定(如何確定參見下一小節)。為對象分配內存的任務等同于把一塊確定大小的內存從java堆中劃分出來。有兩種情況:1.Java堆中內存時絕對規整的,所有使用中的內存在一邊,空閑內存在另一邊,中間有一個指針作為分界點的指示器,則分配內存就是將指針向空閑內存一邊挪動與對象大小相等的距離,這種分配方法成為“指針碰撞(Bump the Pointer)”。2.Java堆中內存并不是規整的,使用中和空閑的內存相互交錯,這時虛擬機必須維護一個用于記錄那些內存快可用的列表,在分配的時候從列表中找到一塊足夠大的空間劃分給對象,并更新列表上的記錄,這種方法稱為“空閑列表(Free List)”。選擇哪種分配方法取決于內存是否規整,內存是否規整又取決于垃圾收集器是否帶有壓縮整理功能。
另一個需要考慮的問題是上述的內存分配過程并不是線程安全的,有兩種方法解決這個問題:1.對分配內存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性。2.把內存分配的動作按照線程劃分在不同的空間中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)。哪個線程要分配內存就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。是否使用TLAB可以通過 -XX:+/-UseTLAB 參數來設定。
內存分配完成后,虛擬機需要將分配的內存空間都初始化為零值,如果使用TLAB,該過程也可以提前到TLAB分配時進行。
接下來,虛擬機要對對象進行必要到設置,例如這個對象時哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)中。
最后,執行<init>方法,按程序員意愿對對象進行初始化,產生一個真正可用的對象。
下面時HotSpot虛擬機bytecodeInterPReter.cpp中的代碼片段:
1 // 確保常量池中存放的是已解釋的類 2 if (!constants->tag_at(index).is_unresolved_klass()) { 3 //斷言確定klassOop和instanceKlassOop 4 oop entry = (klassOop) *constants->obj_at_addr(index); 5 assert(entry->is_klass(), "Should be resolved klass"); 6 klassOop k_entry = (klassOop) entry; 7 assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass"); 8 instanceKlass* ik = (instanceKlass*) k_entry->klass_part(); 9 // 確保對象所屬類型已經經過初始化階段10 if (ik->is_initialized() && ik->can_be_fastpath_allocated()) {11 // 取對象長度12 size_t obj_size = ik->size_helper();13 oop result = NULL;14 bool need_zero = !zeroTLAB;15 if (UserTLAB) {16 result = (oop) THREAD->tlab().allocate(obj_size);17 }18 if (result == NULL) {19 need_zero = true;20 // 直接在eden中分配對象21 retry:22 HeapWord* compare_to = *Universe::heap()->top_addr();23 HeapWord* new_top = compare_to + obj_size;24 // cmpxchg 是x86中的CAS指令,這里是一個c++方法,通過CAS方式分配空間25 // 如果并發失敗,轉到retry中重試,知道成功分配為止26 if (new_top <= *Universe::heap()->end_addr()) {27 if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {28 goto retry;29 }30 result = (oop) compare_to;31 }32 }33 if (result != NULL) {34 // 如果需要,則為對象初始化零值35 if (need_zero) {36 HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;37 obj_size -= sizeof(oopDesc) / oopSize;38 if (obj_size > 0) {39 memset(to_zero, 0, obj_size * HeapWordSize);40 }41 }42 // 根據是否啟用偏向鎖來設置對象頭信息43 if (UseBiasedLocking) {44 result->set_mark(ik->prototype_header();45 } else {46 result->set_mark(markOopDesc::prototype());47 }48 result->set_klass_gap(0);49 result->set_klass(k_entry);50 // 將對象引用入棧,繼續執行下一條指令51 SET_STACK_OBJECT(result, 0);52 UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);53 }54 }55 }
對象的內存布局
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭包括兩部分:
存儲內容 | 標志位 | 狀態 |
對象哈希碼、對象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄信息 | 11 | GC標記 |
偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。包括父類繼承的和子類定義的都需要記錄下來。這部分存儲順序會受虛擬機分配策略參數和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值為true(默認為true),那子類之中較窄的變量也可能會插入到父類變量的空隙之中。
對齊填充并不是必然存在,沒有特別含義,僅僅起占位符的作用。對象的大小必須是8字節的整數倍。
對象的訪問定位
我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象,目前主流的訪問方式有使用句柄和直接指針兩種:
使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象訪問的在Java中非常頻繁,因此這類開銷積小成多也是一項非常可觀的執行成本。
新聞熱點
疑難解答