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虛擬機》第三版(吹爆)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章