文章目錄
- 《深入理解Java虛擬機》讀后筆記-HotSpot虛擬機物件探秘
- 1.物件的創建
- 2.物件的記憶體布局
- 2.1 物件頭
- 2.2 實體資料
- 2.3 對齊填充
- 3.物件的訪問定位
《深入理解Java虛擬機》讀后筆記-HotSpot虛擬機物件探秘
基于實用優先的原則,這里以最常用的虛擬機HotSpot和最常用的記憶體區域Java堆為例,深入探討一下HotSpot虛擬機在Java堆中物件分配、布局和訪問的全程序,
1.物件的創建
在Java語言層面上,創建物件通常(例外:復制、反序列化)僅僅是一個new關鍵字而已:
User user = new User();
而在虛擬機中,物件(文中討論的物件限于普通Java物件,不包括陣列和Class物件等)的創建程序如下:
- 當Java虛擬機遇到一條位元組碼new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號參考,并且檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,
- 在類加載檢查通過后,接下來虛擬機將為新生物件分配記憶體,物件所需記憶體的大小在類加載完成后便可完全確定,為物件分配空間的任務實際上便等同于把一塊確定大小的記憶體塊從Java堆中劃分出來,
- 記憶體分配完成之后,**虛擬機必須將分配到的記憶體空間(但不包括物件頭)都初始化為零值,**如果使用了
TLAB的話,這一項作業也可以提前至TLAB分配時順便進行,這步操作保證了物件的實體欄位 在Java代碼中可以不賦初始值就直接使用,使程式能訪問到這些欄位的資料型別所對應的零值, - 接下來,Java虛擬機還要對物件進行必要的設定,例如這個物件是哪個類的實體、如何才能找到類的元資料資訊、物件的哈希碼(實際上物件的哈希碼會延后到真正呼叫Object.hashCode()方法時才計算)、物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭(Object Header)之中,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,
在上面作業都完成之后,從虛擬機的視角來看,一個新的物件已經產生了,但是從Java程式的視角看來,物件創建才剛剛開始——建構式,即Class檔案中的<init>()方法還沒有執行,所有的欄位都為默認的零值,物件需要的其他資源和狀態資訊也還沒有按照預定的意圖構造好,
關于第二步中為新生物件分配記憶體的兩種方式:
-
指標碰撞
假設Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閑的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閑空間方向挪動一段與物件大小相等的距離
-
空閑串列
但如果Java堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機就必須維護一個串列,記錄上哪些記憶體塊是可用的,在分配的時候從串列中找到一塊足夠大的空間劃分給物件實體,并更新串列上的記錄
選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact〉的能力決定:
- 因此,當使用Serial、ParNew等帶壓縮整理程序的收集器時,系統采用的分配演算法是指標碰撞,既簡單又高效
- 而當使用CMS這種基于清除(Sweep)演算法的收集器時,理論上就只能采用較為復雜的空閑串列來分配記憶體
在并發情況下創建物件會出現的問題及解決方案:
物件創建在虛擬機中是非常頻繁的行為,即使僅僅修改一個指標所指向的位置,在并發情況下也并不是執行緒安全的,可能出現正在給物件 A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況,解決這個問題有兩種可選方案:
- 一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機是采用
CAS配上失敗重試的方式保證更新操作的原子性, - 另外一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩沖(Thread Local Allocation Buffer,
TLAB),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩沖區中分配,只有本地緩沖區用完了,分配新的快取區時才需要同步鎖定,虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB引數來設定,
2.物件的記憶體布局
在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:
物件頭(Header)、實體資料( Instance Data)和對齊填充(Padding),
2.1 物件頭
HotSpot虛擬機物件的物件頭部分包括兩類資訊:
-
第一類是用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機(未開啟壓縮指標)中分別為32個位元和64個位元,官方稱它為“
Mark Word”,如圖:
-
物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,Java虛擬機通過這個指標來確定該物件是哪個類的實體,
需要注意的是:如果物件是一個Java陣列,那在物件頭中還必須有一塊用于記錄陣列長度的資料,因為虛擬機可以通過普通 Java物件的元資料資訊確定Java物件的大小,但是如果陣列的長度是不確定的,將無法通過元資料中的資訊推斷出陣列的大小
2.2 實體資料
實體資料部分是物件真正存盤的有效資訊,
即我們在程式代碼里面所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來,
這部分的存盤順序會受到虛擬機分配策略引數(-XX:FieldsAllocationStyle引數)和欄位在Java原始碼中定義順序的影響,HotSpot虛擬機默認的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上默認的分配策略中可以看到,相同寬度的欄位總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前,如果HotSpot虛擬機的 +XX:CompactFields引數值為true(默認就為true),那子類之中較窄的變數也允許插入父類變數的空隙之中,以節省出一點點空間,
2.3 對齊填充
對齊填充這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用,
由于HotSpot虛擬機的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是任何物件的大小都必須是8位元組的整數倍,物件頭部分已經被精心設計成正好是8位元組的倍數(1倍或者2倍),因此,如果物件實體資料部分沒有對齊的話,就需要通過對齊填充來補全,
3.物件的訪問定位
創建物件自然是為了后續使用該物件,我們的Java程式會通過堆疊上的reference資料來操作堆上的具體物件,主流的訪問方式主要有使用句柄和直接指標兩種:
-
句柄
如果使用句柄訪問的話,Java堆中將可能會劃分出一塊記憶體來作為句柄池,reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自具體的地址資訊,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8Eae5QIg-1640859492671)(《深入理解Java虛擬機》讀后筆記-HotSpot虛擬機物件探秘.assets/image-20211228172542828.png)]](https://img.uj5u.com/2021/12/31/293596310809232.png)
-
直接指標
如果使用直接指標訪問的話,Java堆中物件的記憶體布局就必須考慮如何放置訪問型別資料的相關資訊,reference中存盤的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3OmwNIvF-1640859492673)(《深入理解Java虛擬機》讀后筆記-HotSpot虛擬機物件探秘.assets/image-20211228172553080.png)]](https://img.uj5u.com/2021/12/31/293596310809233.png)
這兩種物件訪問方式各有優勢:
- 使用句柄來訪問的最大好處就是reference中存盤的是穩定句柄地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變句柄中的實體資料指標,而reference本身不需要被修改
- 使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷,由于物件訪問在Java中非常頻繁,因此這類開銷積少成多也是一項極為可觀的執行成本,對HotSpot而言,它主要使用直接指標進行物件訪問

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/398563.html
標籤:其他
上一篇:CUDA unknown error - this may be due to an incorrectly set up environment 問題解決
下一篇:在線運行 Linux,真滴牛逼。
