1. 對象的創建
當Java虛擬機遇到一條字節碼new指令時,就會開始虛擬機中對象的創建:
1.1 類加載檢查
-
檢查new指令的參數是否能在常量池中定位到一個類的符號引用
-
檢查這個符號引用代表的類是否已被加載、解析和初始化過;
如果沒有,那必須先執行相應的類加載過程。
1.2 爲對象分配內存
對象所需內存的大小在類加載完成後便可完全確定,等同於把一塊確定大小的內存塊從Java堆中劃分出來。
選擇哪種分配方式由Java堆是否規整決定,Java堆是否規整又由所採用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。
-
規整
即所有被使用過的內存都被放在一邊,空閒的內存被放在另一邊;
-
不規整
已被使用的內存和空閒的內存相互交錯在一起。
分配方式:
-
指針碰撞 - Bump The Pointer(規整)
已使用內存在一邊,未使用內存在另一邊,中間放一個作爲分界點的指示器;
-
空閒列表 - Free List(不規整)
虛擬機維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。
注意:對象創建在虛擬機中是非常頻繁的操作,即使僅僅修改一個指針所指向的位置,在併發情況下也會引起線程不安全。
線程安全問題解決:
-
對分配內存空間的動作進行同步處理
採用CAS配上失敗重試的方式保證更新操作的原子性;
-
本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)
把內存分配的動作按照線程劃分在不同的空間之中進行,每個線程在Java堆中預先分配一小塊內存,哪個線程要分配內存,就在哪個線程的本地緩衝區中分配,只有本地緩衝區用完了,分配新的緩存區時才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。
1.3 初始化零值
內存分配完成之後,虛擬機必須將分配到的內存空間(但不包括對象頭)都初始化爲零值。
如果使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進行。
保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數據類型所對應的零值。
1.4 對象頭設置
Java虛擬機還要對對象進行必要的設置,存放在對象的對象頭(Object Header)之中:
對象是哪個類的實例、如何找到類的元數據信息、對象的哈希碼(實際上對象的哈希碼會延後到真正調用Object::hashCode()方法時才計算)、對象的GC分代年齡等。
1.5 按意圖初始化
對對象進行必要設置後,從虛擬機的視角來看,一個新的對象已經產生了。Java
程序開發來說,對象創建纔剛開始,需要進行一些初始化操作。new指令之後會接着執行
總結
2.對象的內存佈局
在HotSpot
虛擬機裏,對象在堆內存中的存儲佈局可以劃分爲三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
2.1 對象頭
對象頭主要包含兩部分:
-
用於存儲對象自身的運行時數據(Mark Word);
如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等;
被設計成一個有着動態定義的數據結構,以便在極小的空間內存儲儘量多的數據,根據對象的狀態複用自己的存儲空間。
-
類型指針,即對象指向它的類型元數據的指針。
通過這個指針來確定該對象是哪個類的實例,但並不是所有的虛擬機實現都必須在對象數據上保留類型指針。
如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是如果數組的長度是不確定的,將無法通過元數據中的信息推斷出數組的大小。
2.2 實例數據
對象真正存儲的有效信息,即我們在程序代碼裏面所定義的各種類型的字段內容。
無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。
這部分的存儲順序會受到虛擬機分配策略參數(
-XX:FieldsAllocationStyle
參數)和字段在Java源碼中定義順序的影響。
2.3 對齊填充
佔位符作用。
由於
HotSpot
虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),因此,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。
總結
3. 對象的訪問定位
創建對象後續如何使用該對象?
我們的Java程序會通過棧上的reference數據來操作堆上的具體對象。
由於reference類型在《Java虛擬機規範》裏面只規定了它是一個指向對象的引用,並沒有定義這個引用應該通過什麼方式去定位、訪問到堆中對象的具體位置,所以對象訪問方式也是由虛擬機實現而定的,主流的訪問方式主要有使用句柄和直接指針兩種。
3.1 句柄
Java堆中將可能會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息
3.2 直接指針
reference中存儲的直接就是對象地址。
總結
本文主要介紹了JVM對象創建、對象內存佈局、對象訪問定位,接下來會進一步閱讀《深入理解Java虛擬機》,並進行更多內容的講解、總結。
歡迎點贊/評論,你們的贊同和鼓勵是我寫作的最大動力!