JVM虛擬機-了解Java堆中對象分配、布局和訪問的全過程

前言

對象是Java中最常被提到的概念,也是Java的靈魂,Java中一切皆是對象。

JVM是Java的根基,理解靈魂與根基是如何融合的,對於理解Java本身至關重要。

對象的創建

對象的創建從Java語言層面上,往往就是一個簡單的new即可搞定,而深入JVM的底層,可就複雜的多了。

JVM中對象的創建過程,分為如下5步,如圖:

image-20210508170512540

類載入檢查

虛擬機遇到一條 new 指令後,會執行如下幾個步驟:

  1. 首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用
  2. 檢查這個符號引用代表的類是否已被載入過解析過初始化過
  3. 如果沒有,那必須先執行相應的類載入過程。
  4. 確定對象所需的記憶體大小。
  5. 類載入檢查通過後,進入下一步驟,為新生對象分配記憶體

分配記憶體

為對象分配空間相當於分田地,就是將空餘的記憶體劃分一塊給對象。

由於不同的JVM虛擬機,Java堆的記憶體規整情況是不同的,所以分配方式上也會有一定區別。

Java 堆是否規整,由JVM所採用的垃圾收集器是否帶有壓縮整理功能決定

記憶體空間分配方式

指針碰撞

Java堆中記憶體是規整(無記憶體碎片)時使用,即垃圾收集器有壓縮整理功能時。

原理就是將用過的記憶體放在一塊,沒用的放一塊,中間用指針做標識,分配的時候將指針向空閑記憶體區域移動

空閑列表

Java堆中記憶體不規整,即垃圾收集器無壓縮整理功能時。

使用記憶體和空閑記憶體相互交錯,JVM需維護一個列表,記錄上哪些記憶體是可用的,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。

兩者具像化呈現如圖:

image-20210508174030000

並發時的記憶體分配

創建對象是很頻繁的事,並發創建對象的時候有一個很重要的問題,就是執行緒安全

如:程式中創建對象A和對象B,底層VM給A對象分配記憶體,指針沒來及修改,對象B同時使用原來的指針分配記憶體。

JVM一般採用一下兩種方式來保障執行緒安全。

同步處理:CAS

CAS 是樂觀鎖的一種實現方式。

所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性。

本地執行緒分配緩衝:TLAB

  • 每一個執行緒預先在 Eden 區分配一塊兒記憶體,即為TLAB

  • JVM 在給執行緒中的對象分配記憶體時,首先在 TLAB 分配

  • 當對象大於 TLAB 中的剩餘記憶體不足或已用盡時,再採用上述的 CAS 進行記憶體分配

更多關於CAS的內容可以參考部落客的另一篇文章Java並發/多執行緒-CAS原理分析

初始化零值

記憶體分配完成後,虛擬機需要將分配到的記憶體空間都初始化為零值(不包括對象頭)

這一步操作保證了對象的實例欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的數據類型所對應的零值。

設置對象頭

對象頭是虛擬機對對象進行必要的設置

對象頭存放了關於對象的各種資訊如:

  • 對象是哪個類的實例
  • 如何才能找到類的元數據資訊
  • 對象的哈希碼
  • 對象的 GC 分代年齡
  • ……

根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。

執行 init 方法

在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生

從 Java 程式的視角來看,對象創建才剛開始,<init> 方法還沒有執行,所有的欄位都還為零。

所以一般來說,執行 new 指令之後會接著執行 <init> 方法,把對象按照程式設計師的意願進行初始化,這樣一個真正可用的對象才算完全產生出來。

對象的記憶體布局

在 Hotspot 虛擬機中,對象在記憶體中的布局可以分為 3 塊區域。

對象頭

對象頭上面已經介紹過了,其實主要就是分為兩類:

  • 用於存儲對象自身的運行時數據
  • 類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例

實例數據

這部分是對象真正存儲的有效資訊,也是在程式中所定義的各種類型的欄位內容。

對齊填充

對齊填充不是必然存在的,沒有實際意義,就是用來補位的。

因為 Hotspot 虛擬機的自動記憶體管理系統要求對象起始地址必須是 8 位元組的整數倍,對象的大小必須是 8 位元組的整數倍。

所以,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位

建立對象就是為了使用對象,我們的 Java 程式通過棧上的 reference 數據來操作堆上的具體對象。

對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有如下兩種方式

使用句柄

Java 堆中將會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址資訊,如圖:

image-20210508181954450

直接指針

如果使用直接指針訪問,那麼 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關資訊,而 reference 中存儲的直接就是對象的地址。

image-20210508181849041

二者對比

使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。

使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

參考:《深入理解JVM虛擬機》第三版(吹爆)

Tags: