標籤:同步鎖 識別 opd 共用體 open 收集 垃圾收集 分代 分配
相關文章
Java虛擬機器系列
前言
在前一篇文章中我們學習了Java虛擬機器的結構原理與運行時資料區域,那麼我們大概知道了Java虛擬機器的記憶體的概況,那麼記憶體中的資料是如何建立和訪問的呢?這篇文章會給你答案。
1.對象的建立
對象的建立通常是通過new一個對象而已,當虛擬機器接收到一個new指令時,它會做如下的操作。
(1)判斷對象對應的類是否載入、連結、初始化
虛擬機器接收到一條new指令時,首先會去檢查這個指定的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被類載入器載入、連結和初始化過。如果沒有則先執行相應的類載入過程。關於類載入器我們在前一篇文章中已經提到過,這裡不再贅述。
(2)為對象分配記憶體
類載入完成後,接著會在Java堆中劃分一塊記憶體配置給對象。記憶體配置根據Java堆是否規整,有兩種方式:
- 指標碰撞:如果Java堆的記憶體是規整,即所有用過的記憶體放在一邊,而閒置的放在另一邊。分配記憶體時將位於中間的指標指標向閒置記憶體移動一段與對象大小相等的距離,這樣便完成分配記憶體工作。
- 空閑列表:如果Java堆的記憶體不是規整的,則需要由虛擬機器維護一個列表來記錄那些記憶體是可用的,這樣在分配的時候可以從列表中查詢到足夠大的記憶體配置給對象,並在分配後更新列表記錄。
Java堆的記憶體是否規整根據所採用的來及收集器是否帶有壓縮整理功能有關,關於垃圾收集器,本系列後面的文章會介紹。
(3)處理並發安全問題
建立對象是一個非常頻繁的操作,所以需要解決並發的問題,有兩種方式:
- 對分配記憶體空間的動作進行同步處理,比如在虛擬機器採用CAS演算法並配上失敗重試的方式保證更新操作的原子性。
- 每個線程在Java堆中預先分配一小塊記憶體,這塊記憶體稱為本地線程分配緩衝(Thread Local Allocation Buffer)簡寫為TLAB,線程需要分配記憶體時,就在對應線程的TLAB上分配記憶體,當線程中的TLAB用完並且被分配到了新的TLAB時,這時候才需要同步鎖定。通過-XX:+/-UserTLAB參數來設定虛擬機器是否使用TLAB。
(4)初始化分配到的記憶體空間
將分配到的記憶體,除了對象頭都初始化為零值。
(5)設定對象的對象頭
將對象的所屬類、對象的HashCode和對象的GC分代年齡等資料存放區在對象的對象頭中。
(6)執行init方法進行初始化
執行init方法,初始化對象的成員變數、調用類的構造方法,這樣一個對象就被建立了出來。
2.對象的堆記憶體布局
對象建立完畢,並且已經在Java堆中分配了記憶體,那麼對象在堆記憶體是如何進行布局的呢?
以HotSpot虛擬機器為例,對象在堆記憶體的布局分為三個地區,分別是對象頭(Header)、執行個體資料(Instance Data)、對齊填充(Padding)。
- 對象頭:對象頭包括兩部分資訊分別是Mark World和中繼資料指標,Mark World用於儲存物件運行時的資料,比如HashCode、鎖狀態標誌、GC分代年齡等。而中繼資料指標用於指向方法區的中目標類的類型資訊,通過中繼資料指標可以確定對象的具體類型。
- 執行個體資料:用於儲存物件中的各種類型的欄位資訊(包括從父類繼承來的)。
- 對齊填充:對齊填充不一定存在,起到了預留位置的作用,沒有特別的含義。
對象的記憶體布局如所示。
3.HotSpot的物件模型
HotSpot中採用了OOP-Klass模型,它是用來描述Java對象執行個體的一種模型,OOP(Ordinary Object Pointer)指的是普通對象指標,而Klass用來描述對象執行個體的具體類型。
HotSpot中,用instanceOopDesc 和 arrayOopDesc 來描述對象頭,其中arrayOopDesc對象用於描述數群組類型。
instanceOopDesc的代碼如下所示。
openjdk/hotspot/src/share/vm/oops/instanceOop.hpp
class instanceOopDesc : public oopDesc { public: // aligned header size. static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned. static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); } static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); }};
可以看出instanceOopDesc繼承自oopDesc:
openjdk/hotspot/src/share/vm/oops/oop.hpp
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set. Must be initialized. static BarrierSet* _bs;...}
oopDesc中包含兩個資料成員:_mark 和 _metadata。其中markOop類型的_mark對象指的是前面講到的Mark World。_metadata是一個共用體,其中_klass是普通指標,_compressed_klass是壓縮類指標,它們就是前面講到的中繼資料指標,這兩個指標都指向instanceKlass對象,它用來描述對象的具體類型。
instanceKlass的代碼如下所示。
openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass { ... enum ClassState { allocated, // allocated (but not yet linked) loaded, // loaded and inserted in class hierarchy (but not linked yet) linked, // successfully linked/verified (but not initialized yet) being_initialized, // currently running class initializer fully_initialized, // initialized (successfull final state) initialization_error // error happened during initialization }; ... }
instanceKlass繼承自Klass ,枚舉ClassState 用來標識對象的載入進度。
知道了OOP-Klass模型,我們就可以分析Java虛擬機器是如何通過棧幀中的對象引用找到對應的對象執行個體,如所示。
可以看出,通過棧幀中的對象引用找到Java堆中的instanceOopDesc對象,再通過instanceOopDesc中的中繼資料指標來找到方法區中的instanceKlass,從而確定該對象的具體類型。
參考資料
《深入理解Java虛擬機器》
《JAVA虛擬機器精講》
深入探究 JVM | klass-oop 物件模型研究
JVM源碼分析之Java對象的建立過程
JVM源碼分析之Java類的載入過程
歡迎關注我的公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。
Java虛擬機器(二)對象的建立與OOP-Klass模型