文章目錄
- 1. 物件的創建
- 1.1 類加載
- 1.2 分配記憶體
- (1)、分配記憶體的方式
- (2)、分配記憶體的并發問題
- 1.3 初始化零值
- 1.4 設定物件頭
- 1.5 執行< init >方法
- 2. 物件的記憶體布局
- 2.1 物件頭
- 2.2 實體資料
- 2.3 對齊填充
- 3. 物件的訪問定位
- 3.1 句柄
- 3.2 直接指標
本文參考于《深入理解Java虛擬機》
1. 物件的創建
物件的創建主要分為五個部分:類加載、分配記憶體、初始化零值、設定物件頭和執行< init >方法,接下來,對物件的創建的講解我們將從這五個部分展開,
1.1 類加載
當Java虛擬機遇到一條位元組碼new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號參考,并且檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,
1.2 分配記憶體
在類加載檢查通過后,接下來虛擬機將為新生物件分配記憶體,物件所需記憶體的大小在類加載完成后便可完全確定,為物件分配空間的任務實際上便等同于把一塊確定大小的記憶體塊從Java堆中劃分出來,
(1)、分配記憶體的方式
記憶體的分配方式不同取決于Java堆中記憶體是否規整,
- 指標碰撞 : 假設
Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閑的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閑空間方向挪動一段與物件大小相等的距離, - 空閑串列:但如果
Java堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機就必須維護一個串列,記錄上哪些記憶體塊是可用的,在分配的時候從串列中找到一塊足夠大的空間劃分給物件實體,并更新串列上的記錄,
而
Java堆中記憶體是否規整又取決于Java堆進行垃圾回收時使用哪種垃圾收集器,
- 指標碰撞:使用
Serial、ParNew等帶壓縮整理程序的收集器, - 空閑串列:使用
CMS這種基于清除(Sweep)演算法的收集器,
(2)、分配記憶體的并發問題
- 可能出現的問題
物件創建在虛擬機中是非常頻繁的行為,即使僅僅修改一個指標所指向的位置,在并發情況下也并不是執行緒安全的,可能出現正在給物件A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況,
- 保障執行緒安全的兩種方法
- 一種是
對分配記憶體空間的動作進行同步處理——實際上虛擬機是采用CAS配上失敗重試的方式保證更新操作的原子性,CAS就是樂觀鎖的一種實作方式,樂觀鎖就是每個執行緒中都有一個執行緒共享變數的版本變數,當一個執行緒需要對該變數進行操作時,它會首先進行比較版本變數是否相同,如果相同就進行操作;如果不同就會造成沖突而失敗,而該方法中如果因為沖突而造成失敗就會一直重試,直到成功為止,從而保證了更新操作的原子性, - 是把
記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩沖(Thread Local AllocationBuffer,TLAB),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩沖區中分配,只有本地緩沖區用完了,分配新的快取區時才需要同步鎖定,虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB引數來設定,
1.3 初始化零值
記憶體分配完成之后,虛擬機必須將分配到的記憶體空間(但不包括物件頭)都初始化為零值,這步操作保證了物件的實體欄位在Java代碼中可以不賦初始值就直接使用,使程式能訪問到這些欄位的資料型別所對應的零值,
1.4 設定物件頭
Java虛擬機還要對物件進行必要的設定,例如這個物件是哪個類的實體、如何才能找到類的元資料資訊、物件的哈希碼(實際上物件的哈希碼會延后到真正呼叫Object::hashCode()方法時才計算)、物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭(Object Header)之中,根據虛擬機當前運狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,關于物件頭的具體內容,稍后會詳細介紹,
1.5 執行< init >方法
在上面作業都完成之后,從虛擬機的視角來看,一個新的物件已經產生了,但是從Java程式的視角看來,物件創建才剛剛開始——建構式,即Class檔案中的< init >()方法還沒有執行,所有的欄位都為默認的零值,物件需要的其他資源和狀態資訊也還沒有按照預定的意圖構造好(也就是物件相應的屬性欄位還是初始化的零值),一般來說,new指令之后會接著執行< init >()方法,按照程式員的意愿對物件進行初始化,這樣一個真正可用的物件才算完全被構造出來,
2. 物件的記憶體布局
在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體 資料(Instance Data)和對齊填充(Padding),下面我們將從這三個方面認識物件的記憶體布局,
2.1 物件頭
物件頭主要包含兩部分資訊:
- 第一類是用于存盤物件自身的運行時資料,如
哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,官方稱這部分資料為 “Mark Word” ,如下圖所示,

- 物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,Java虛擬機通過這個指標來確定該物件是哪個類的實體,
2.2 實體資料
實體資料部分是物件真正存盤的有效資訊,即我們在程式代碼里面所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來,這部分的存盤順序會受到虛擬機分配策略引數(-XX:FieldsAllocationStyle引數)和欄位在Java原始碼中定義順序的影響,
2.3 對齊填充
物件的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用,其實就是HotSpot虛擬機自動記憶體管理系統要求物件的大小是8位元組的倍數,因此就誕生了對齊填充,如果不是8位元組的倍數,就進行填充補全,
3. 物件的訪問定位
Java程式會通過堆疊上的reference資料來操作堆上的具體物件,主流的訪問方式主要有使用句柄和直接指標,
3.1 句柄
如果使用句柄訪問的話,Java堆中將可能會劃分出一塊記憶體來作為句柄池,reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自具體的地址資訊,
1. 如下圖所示

2. 使用好處
使用句柄來訪問的最大好處就是reference中存盤的是穩定句柄地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變句柄中的實體資料指標,而reference本身不需要被修改,
3.2 直接指標
·如果使用直接指標訪問的話,Java堆中物件的記憶體布局就必須考慮如何放置訪問型別資料的相關資訊,reference中存盤的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷,
1. 如下圖所示

2. 使用好處
使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷,因為如果是訪問物件本身的話,reference中存盤的直接就是物件地址,我們只需要訪問一次就夠了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/356873.html
標籤:其他
