相關文章
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相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。