天天看點

深入了解JVM虛拟機讀書筆記——對象的建立與記憶體布局

1. 對象的建立過程

在 Java 語言層面,建立對象一般是借助

new

關鍵字去實作:

User user = new User();
      

而在虛拟機中,對象的建立過程如下:

當Java虛拟機遇到一條位元組碼new指令時,首先将去檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程(類加載這部分後面的文章會詳細介紹)。

在類加載檢查通過後,接下來需要為新生對象配置設定記憶體:為對象配置設定空間的任務實際上便等同于把一塊确定

大小的記憶體塊從 Java 堆中劃分出來。

記憶體配置設定完成之後,虛拟機必須将配置設定到的記憶體空間(但不包括對象頭)都初始化為零值。這步操作保證了對象的執行個體字段在 Java 代碼中可以不賦初始值就直接使用,使程式能通路到這些字段的資料類型所對應的零值。

接下來,Java虛拟機還要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼(實際上對象的哈希碼會延後到真正調用Object::hashCode()方法時才計算)、對象的GC分代年齡等資訊。這些資訊存放在對象的對象頭(Object Header)之中。根據虛拟機目前運作狀态的不同,如是否啟用偏向鎖等,對象頭會有不同的設定方式。關于對象頭的具體内容,稍後會詳細介紹。

在上面工作都完成之後,從虛拟機的視角來看,一個新的對象已經産生了。但是從Java程式的視角看來,對象建立才剛剛開始——構造函數,即Class檔案中的< init >()方法還沒有執行,所有的字段都為預設的零值,構造函數的位元組碼指令這部分後面文章會詳細分析。

這裡要重點介紹一下為新生對象配置設定記憶體的2種方式:

1.1 ”指針碰撞“

如果 Java 堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閑的記憶體被放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離,這種配置設定方式稱為“指針碰撞”(Bump The Pointer)。

1.2 “空閑清單”

如果 Java 堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體互相交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄,這種配置設定方式稱為“空閑清單”(Free List)。

上述2種方式,選擇哪種配置設定方式是由 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 對象頭

對象的對象頭部分包括兩類資訊:

第一類是用于存儲對象自身的運作時資料,如:哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,官方稱它為 “Mark Word”。

深入了解JVM虛拟機讀書筆記——對象的建立與記憶體布局
深入了解JVM虛拟機讀書筆記——對象的建立與記憶體布局

對象頭的另外一部分是類型指針,即對象指向它的類型中繼資料的指針,Java虛拟機通過這個指針來确定該對象是哪個類的執行個體。

注意:

如果對象是一個 Java 數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通 Java 對象的中繼資料資訊确定 Java 對象的大小,但是如果數組的長度是不确定的,将無法通過中繼資料中的資訊推斷出數組的大小。

2.2 執行個體資料

執行個體資料部分是對象真正存儲的有效資訊,即我們在程式代碼裡面所定義的各種類型的字段内容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。

2.3 對其填充

對象的第三部分是對齊填充,這并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。

由于HotSpot虛拟機的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說就是任何對象的大小都必須是8位元組的整數倍。對象頭部分已經被精心設計成正好是8位元組的倍數(1倍或者2倍),是以,如果對象執行個體資料部分沒有對齊的話,就需要通過對齊填充來補全。

3. 對象的通路定位

Java程式會通過棧上的 reference 資料(對象引用)來操作堆上的具體對象。而對象引用操作具體對象的方式有以下2種:

句柄,如果使用句柄通路的話,Java堆中将可能會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自具體的位址資訊,其結構如圖所示:

深入了解JVM虛拟機讀書筆記——對象的建立與記憶體布局
  • 直接指針,如果使用直接指針通路的話,Java堆中對象的記憶體布局就必須考慮如何放置通路類型資料的相關資訊,reference 中存儲的直接就是對象位址,如果隻是通路對象本身的話,就不需要多一次間接通路的開銷,如圖所示:
深入了解JVM虛拟機讀書筆記——對象的建立與記憶體布局

這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而 reference 本身不需要被修改。

使用直接指針來通路最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象通路在 Java 中非常頻繁,是以這類開銷積少成多也是一項極為可觀的執行成本,HotSpot 虛拟機主要使用的是直接指針的方式。