深入理解Java對(duì)象:從創(chuàng)建到內(nèi)存訪問(wèn)的JVM底層機(jī)制
先去看看這篇博客了解下運(yùn)行時(shí)JVM數(shù)據(jù)區(qū)域,然后再回來(lái)看下面內(nèi)容,??????記得先贊后看效果翻倍?????? ~
引言
在Java開(kāi)發(fā)中,new關(guān)鍵字是我們創(chuàng)建對(duì)象最常用的方式。然而,在這簡(jiǎn)單的操作背后,JVM進(jìn)行了一系列復(fù)雜而精妙的操作。許多開(kāi)發(fā)者雖然每天都在創(chuàng)建對(duì)象,但對(duì)于對(duì)象在JVM中是如何被創(chuàng)建、如何在內(nèi)存中布局以及如何被訪問(wèn)的底層細(xì)節(jié)知之甚少。理解這些底層機(jī)制不僅有助于我們編寫(xiě)更高效的代碼,還能在性能調(diào)優(yōu)和故障排查時(shí)提供關(guān)鍵洞察,幫助我們更好地理解Java程序的運(yùn)行原理。
本文將深入JVM層面,全面解析Java對(duì)象的完整生命周期:從創(chuàng)建過(guò)程、內(nèi)存布局到訪問(wèn)定位方式。通過(guò)本文,您將獲得對(duì)Java對(duì)象在JVM中表現(xiàn)的全面認(rèn)識(shí)。
一、對(duì)象的創(chuàng)建過(guò)程
1.1 類(lèi)加載檢查
當(dāng)JVM遇到一條new指令時(shí)(例如new MyClass()),它首先檢查這個(gè)指令的參數(shù)是否能在運(yùn)行時(shí)常量池中定位到一個(gè)類(lèi)的符號(hào)引用。
- 檢查內(nèi)容:檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載、解析和初始化過(guò)
- 如果未加載:如果JVM發(fā)現(xiàn)這個(gè)類(lèi)還沒(méi)有被加載,它會(huì)立即先執(zhí)行類(lèi)的加載過(guò)程(Loading → Linking → Initialization)。這是一個(gè)相對(duì)耗時(shí)的操作,包括讀取類(lèi)文件、驗(yàn)證、準(zhǔn)備解析等步驟
- 如果已加載:則直接進(jìn)入下一步,為新生對(duì)象分配內(nèi)存
這一步確保了對(duì)象一定是基于一個(gè)已被JVM完全知曉和驗(yàn)證的類(lèi)創(chuàng)建的。
1.2 內(nèi)存分配
在類(lèi)加載檢查通過(guò)后,JVM將為新生對(duì)象分配內(nèi)存。所謂分配內(nèi)存,就是從Java堆中劃出一塊確定大小的內(nèi)存空間給這個(gè)新對(duì)象。分配方式取決于Java堆是否規(guī)整,而堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理的能力決定。
主要有兩種分配方式:
a) 指針碰撞
- 條件:假設(shè)Java堆的內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器
- 操作:分配內(nèi)存僅僅就是把那個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離
- 收集器:Serial, ParNew等帶有壓縮整理功能的收集器
b) 空閑列表
- 條件:如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)
- 操作:JVM必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊是可用的。在分配時(shí),從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄
- 收集器:CMS這種基于標(biāo)記-清除算法的收集器
內(nèi)存分配中的并發(fā)問(wèn)題
對(duì)象創(chuàng)建在JVM中非常頻繁,即使在單線程環(huán)境下,僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線程安全的。可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存。
JVM采用了兩種方案來(lái)解決這個(gè)問(wèn)題:
- CAS + 失敗重試:對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理,采用比較并交換(Compare And Swap)算法保證原子性。這是現(xiàn)代虛擬機(jī)普遍采用的方式
- TLAB:本地線程分配緩沖。每個(gè)線程在Java堆中預(yù)先分配一小塊私有內(nèi)存。哪個(gè)線程要分配內(nèi)存,就在自己的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。可以通過(guò)
-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定是否啟用
1.3 內(nèi)存空間初始化(零值初始化)
內(nèi)存分配完成后,JVM會(huì)將分配到的內(nèi)存空間(不包括對(duì)象頭)都初始化為零值(0, null, false等)。
- 目的:這步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值
- 例如:
int會(huì)初始化為0,boolean初始化為false,所有引用類(lèi)型會(huì)初始化為null
注意:此時(shí)對(duì)象還沒(méi)有開(kāi)始執(zhí)行Java代碼中定義的構(gòu)造方法。
1.4 設(shè)置對(duì)象頭
Java對(duì)象在內(nèi)存中的存儲(chǔ)布局可以分為三部分:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
在零值初始化之后,JVM需要對(duì)這個(gè)新生對(duì)象的對(duì)象頭進(jìn)行設(shè)置。對(duì)象頭包含兩類(lèi)信息:
-
Mark Word:用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)
- 哈希碼
- GC分代年齡
- 鎖狀態(tài)標(biāo)志
- 線程持有的鎖
- 偏向線程ID
- 偏向時(shí)間戳
- 等等
- 這部分?jǐn)?shù)據(jù)在32位和64位的虛擬機(jī)中分別為32bit和64bit,它的結(jié)構(gòu)是非固定的,會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,以節(jié)省空間
-
類(lèi)型指針:即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針。JVM通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例
- 并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類(lèi)型指針(即通過(guò)指針訪問(wèn)對(duì)象類(lèi)型),這取決于對(duì)象的訪問(wèn)定位方式
-
數(shù)組長(zhǎng)度:如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)
1.5 執(zhí)行構(gòu)造方法
從JVM的視角看,執(zhí)行構(gòu)造方法<init>是對(duì)象創(chuàng)建的最后一步。
<init>方法:Java編譯器會(huì)將實(shí)例變量初始化器(例如int a = 123;)和構(gòu)造方法塊({})的代碼,收集合并到一個(gè)名為<init>的特殊方法中- 執(zhí)行過(guò)程:JVM執(zhí)行
<init>方法,按照開(kāi)發(fā)者的意圖對(duì)對(duì)象進(jìn)行初始化,也就是為對(duì)象的字段賦予程序員真正想要的初始值 - 與
<clinit>的區(qū)別:<init>是實(shí)例構(gòu)造器,用于初始化對(duì)象。而<clinit>是類(lèi)構(gòu)造器,用于初始化類(lèi)變量(static變量)
直到這一步,一個(gè)真正可用的、符合開(kāi)發(fā)者預(yù)期的對(duì)象才被完全構(gòu)造出來(lái)。
二、對(duì)象的內(nèi)存布局
Java對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以分為三個(gè)連續(xù)的區(qū)域:
- 對(duì)象頭
- 實(shí)例數(shù)據(jù)
- 對(duì)齊填充
2.1 對(duì)象頭
對(duì)象頭包含了JVM用于管理對(duì)象所必需的運(yùn)行時(shí)數(shù)據(jù)。它本身又由三部分組成:Mark Word、類(lèi)型指針,以及數(shù)組長(zhǎng)度(如果是數(shù)組對(duì)象)。
a) Mark Word
這是對(duì)象頭中最重要的部分,用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)。它的長(zhǎng)度在32位JVM上是32位,在64位JVM上是64位。
為了實(shí)現(xiàn)空間高效利用,Mark Word的設(shè)計(jì)是非固定的,會(huì)根據(jù)對(duì)象的狀態(tài)在不同時(shí)刻存儲(chǔ)不同的內(nèi)容,以復(fù)用自己的存儲(chǔ)空間。下表展示了在64位JVM下,Mark Word在不同狀態(tài)下的存儲(chǔ)內(nèi)容:
| 鎖狀態(tài) (Lock State) | 25 bits (64位JVM) | 31 bits (64位JVM) | 1 bit (cms_free) | 4 bits (分代年齡) | 1 bit (偏向鎖標(biāo)志) | 2 bits (鎖標(biāo)志位) |
|---|---|---|---|---|---|---|
| 無(wú)鎖 (Unlocked) | unused | identity_hashcode | 分代年齡 (age) | 0 | 01 | |
| 偏向鎖 (Biased) | thread_id (54 bits) | epoch (2 bits) | 分代年齡 (age) | 1 | 01 | |
| 輕量級(jí)鎖 (Lightweight Locked) | 指向棧中鎖記錄的指針 (ptr_to_lock_record) | 00 | ||||
| 重量級(jí)鎖 (Heavyweight Locked) | 指向監(jiān)視器(管程/互斥量)的指針 (ptr_to_heavyweight_monitor) | 10 | ||||
| GC標(biāo)記 (Marked for GC) | unused | 11 |
- 身份哈希碼:調(diào)用
Object.hashCode()或System.identityHashCode()后計(jì)算并存儲(chǔ)的結(jié)果。一旦存入,該值會(huì)一直伴隨該對(duì)象 - 分代年齡:對(duì)象在Survivor區(qū)每熬過(guò)一次Minor GC,年齡就增加1。此值有4位,最大為15,這就是為什么-XX:MaxTenuringThreshold的默認(rèn)最大值是15
- 鎖信息:synchronized鎖的等級(jí)(偏向鎖、輕量級(jí)鎖、重量級(jí)鎖)相關(guān)的線程ID、鎖記錄指針、重量級(jí)鎖監(jiān)視器指針等
- GC狀態(tài):標(biāo)記該對(duì)象是否被垃圾回收器標(biāo)記為可回收狀態(tài)
b) 類(lèi)型指針
- 即對(duì)象指向它的類(lèi)型元數(shù)據(jù)的指針
- JVM通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例
- 并不是所有JVM實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類(lèi)型指針(這取決于對(duì)象的訪問(wèn)定位方式,例如使用句柄池就不需要),但通過(guò)直接指針(HotSpot默認(rèn)方式)訪問(wèn)則必須存在
- 在64位JVM上,如果開(kāi)啟了壓縮指針(-XX:+UseCompressedOops,默認(rèn)開(kāi)啟),該指針的長(zhǎng)度為4字節(jié)(32位),否則為8字節(jié)(64位)
c) 數(shù)組長(zhǎng)度
- 只有數(shù)組對(duì)象才有
- 用于記錄數(shù)組的長(zhǎng)度,占4字節(jié)(32位)
- 有了這個(gè)字段,JVM就可以從數(shù)組對(duì)象的元數(shù)據(jù)中確定數(shù)組的大小,而不需要必須通過(guò)類(lèi)型元數(shù)據(jù)
對(duì)象頭大小總結(jié):
- 在64位JVM(開(kāi)啟壓縮指針)下:
- 普通對(duì)象:Mark Word(8字節(jié)) + 類(lèi)型指針(4字節(jié)) = 12字節(jié)
- 數(shù)組對(duì)象:Mark Word(8字節(jié)) + 類(lèi)型指針(4字節(jié)) + 數(shù)組長(zhǎng)度(4字節(jié)) = 16字節(jié)
- 在64位JVM(關(guān)閉壓縮指針)下:
- 普通對(duì)象:Mark Word(8字節(jié)) + 類(lèi)型指針(8字節(jié)) = 16字節(jié)
- 數(shù)組對(duì)象:Mark Word(8字節(jié)) + 類(lèi)型指針(8字節(jié)) + 數(shù)組長(zhǎng)度(4字節(jié)) + 對(duì)齊填充(4字節(jié)) = 24字節(jié)
2.2 實(shí)例數(shù)據(jù)
- 這是對(duì)象真正存儲(chǔ)的有效信息,即我們?cè)诔绦虼a里所定義的各種類(lèi)型的字段內(nèi)容,無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的,都必須記錄起來(lái)
- 存儲(chǔ)順序:JVM默認(rèn)會(huì)按照以下規(guī)則對(duì)字段進(jìn)行排序:
- 父類(lèi)定義的變量會(huì)出現(xiàn)在子類(lèi)之前
- 較寬的變量(如double/long)通常會(huì)被分配在更靠前的位置(但HotSpot VM較新版本的一些優(yōu)化策略可能會(huì)有所調(diào)整)
- 如果開(kāi)啟了
-XX:CompactFields參數(shù)(默認(rèn)開(kāi)啟),JVM允許子類(lèi)中較窄的變量插入到父類(lèi)變量的空隙中,以節(jié)省空間
- 這部分的大小完全由類(lèi)的字段定義決定
2.3 對(duì)齊填充
- 這部分不是必須存在的,僅僅起著占位符的作用
- 目的:HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。換句話說(shuō),就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍
- 對(duì)象頭和實(shí)例數(shù)據(jù)部分結(jié)束后,如果整個(gè)對(duì)象的大小還不是8字節(jié)的整數(shù)倍,那么對(duì)齊填充就會(huì)發(fā)揮作用,將剩余的空間補(bǔ)足
- 這完全是出于性能考慮,讓CPU讀取數(shù)據(jù)更加高效(例如,一次總線事務(wù)可以讀取完整的數(shù)據(jù))
對(duì)象內(nèi)存布局可視化:
2.4 實(shí)戰(zhàn):使用JOL分析對(duì)象布局
OpenJDK提供了Java Object Layout (JOL) 工具包,可以讓我們直觀地查看對(duì)象的內(nèi)存布局。
示例代碼:
首先引入JOL庫(kù)(如果使用Maven):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
<scope>provided</scope>
</dependency>
然后編寫(xiě)分析代碼:
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class ObjectLayoutDemo {
public static void main(String[] args) {
// 打印JVM詳情(例如是否開(kāi)啟壓縮指針)
System.out.println(VM.current().details());
// 分析一個(gè)簡(jiǎn)單的對(duì)象
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 分析一個(gè)自定義對(duì)象
class MyClass {
private int id;
private String name;
private boolean flag;
// 未使用的填充空間可能會(huì)被JVM優(yōu)化
}
MyClass myObj = new MyClass();
System.out.println(ClassLayout.parseInstance(myObj).toPrintable());
}
}
可能的輸出(在64位JVM,開(kāi)啟壓縮指針下):
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # (Mark Word: 無(wú)鎖狀態(tài),identity_hashcode未計(jì)算)
4 4 (object header) 00 00 00 00 # (Mark Word 繼續(xù))
8 4 (object header) e5 01 00 f8 # (類(lèi)型指針,壓縮后4字節(jié))
12 4 (loss due to the next object alignment) <-- 對(duì)齊填充
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
ObjectLayoutDemo$1MyClass object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 77 1b 01 f8 # 類(lèi)型指針
12 4 int MyClass.id 0
16 1 boolean MyClass.flag false
17 3 (alignment/padding gap) # 為了將后面的引用對(duì)齊到8字節(jié)而做的填充
20 4 java.lang.String MyClass.name null
24 4 (loss due to the next object alignment) # 對(duì)象總大小24字節(jié),需要填充到8的倍數(shù)(24已是8的倍數(shù),這里可能無(wú)填充或顯示0)
Instance size: 24 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
從輸出中可以清晰地看到:
Object對(duì)象:12字節(jié)對(duì)象頭 + 4字節(jié)填充 = 16字節(jié)MyClass對(duì)象:12字節(jié)對(duì)象頭 + 4字節(jié)(int) + 1字節(jié)(boolean) + 3字節(jié)(對(duì)齊間隙) + 4字節(jié)(壓縮后的引用) = 24字節(jié)。內(nèi)部的3字節(jié)填充是為了讓name引用字段從20字節(jié)開(kāi)始(不是8的倍數(shù)),跳到24字節(jié)(是8的倍數(shù)),這樣CPU訪問(wèn)效率更高
三、對(duì)象的訪問(wèn)定位
對(duì)象的訪問(wèn)定位探討的是這樣一個(gè)核心問(wèn)題:棧上的reference類(lèi)型數(shù)據(jù)(即我們通常所說(shuō)的"對(duì)象引用"或"指針")如何準(zhǔn)確地定位到堆中對(duì)象實(shí)例的具體位置?
JVM規(guī)范只規(guī)定了reference類(lèi)型是一個(gè)指向?qū)ο蟮囊茫⑽炊x這個(gè)引用應(yīng)該通過(guò)何種方式去定位、訪問(wèn)堆中對(duì)象的具體位置。主流的JVM實(shí)現(xiàn)主要有兩種方式:
- 使用句柄訪問(wèn)
- 使用直接指針訪問(wèn)
HotSpot VM主要采用第二種方式,但理解第一種方式對(duì)于對(duì)比和深入理解至關(guān)重要。
3.1 使用句柄訪問(wèn)
如果使用句柄方式,Java堆將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池。
reference中存儲(chǔ)的是對(duì)象的句柄地址- 句柄本身包含了對(duì)象實(shí)例數(shù)據(jù)的指針和對(duì)象類(lèi)型數(shù)據(jù)的指針
其內(nèi)存結(jié)構(gòu)和訪問(wèn)過(guò)程如下圖所示:
優(yōu)點(diǎn):
- 引用穩(wěn)定:
reference本身存儲(chǔ)的是穩(wěn)定的句柄地址。當(dāng)對(duì)象被垃圾收集器移動(dòng)時(shí)(例如在標(biāo)記-壓縮或復(fù)制算法中),只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要做任何修改。這對(duì)于那些需要頻繁進(jìn)行GC優(yōu)化(如壓縮堆)的場(chǎng)景非常友好
缺點(diǎn):
- 訪問(wèn)速度相對(duì)較慢:訪問(wèn)對(duì)象實(shí)例數(shù)據(jù)需要兩次指針定位(先定位到句柄,再通過(guò)句柄定位到實(shí)例數(shù)據(jù)),這比直接指針訪問(wèn)多了一次內(nèi)存尋址開(kāi)銷(xiāo)。而對(duì)象訪問(wèn)在Java程序中是非常頻繁的操作,因此這種開(kāi)銷(xiāo)會(huì)被放大
3.2 使用直接指針訪問(wèn)(HotSpot采用的方式)
如果使用直接指針?lè)绞剑敲碕ava堆對(duì)象的布局中就必須考慮如何放置訪問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息。
reference中存儲(chǔ)的就是對(duì)象的直接地址- 對(duì)象實(shí)例數(shù)據(jù)中需要包含一個(gè)指向方法區(qū)中對(duì)象類(lèi)型數(shù)據(jù)的指針(即對(duì)象頭中的類(lèi)型指針)
其內(nèi)存結(jié)構(gòu)和訪問(wèn)過(guò)程如下圖所示:
優(yōu)點(diǎn):
- 訪問(wèn)速度更快:相比于句柄方式,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo)。由于對(duì)象訪問(wèn)是Java程序中最頻繁的操作之一,因此這類(lèi)開(kāi)銷(xiāo)的減少對(duì)性能的提升是非常可觀的
缺點(diǎn):
- 引用不穩(wěn)定:當(dāng)對(duì)象被移動(dòng)時(shí)(例如GC后內(nèi)存整理),
reference本身存儲(chǔ)的地址需要被直接更新。如果有很多地方都引用了這個(gè)對(duì)象,更新這些引用的開(kāi)銷(xiāo)會(huì)比較大(不過(guò)現(xiàn)代GC算法如Shenandoah、ZGC等,通過(guò)讀屏障等技術(shù)極大地優(yōu)化了這個(gè)問(wèn)題)
3.3 HotSpot VM的實(shí)現(xiàn)與優(yōu)化
HotSpot VM主要使用直接指針?lè)绞竭M(jìn)行對(duì)象訪問(wèn)。
你可能會(huì)問(wèn),它如何解決直接指針的"引用不穩(wěn)定"這個(gè)缺點(diǎn)呢?答案是:通過(guò)復(fù)雜的GC算法和精巧的實(shí)現(xiàn)來(lái)協(xié)同解決。
-
針對(duì)移動(dòng)對(duì)象的處理:
- 在發(fā)生垃圾回收,特別是需要壓縮堆(如使用Serial, ParNew, G1等收集器的老年代回收)時(shí),HotSpot確實(shí)需要更新所有指向被移動(dòng)對(duì)象的引用
- 這個(gè)過(guò)程是由GC器通過(guò)記憶集和卡表等技術(shù)來(lái)跟蹤哪些引用需要更新,并在STW階段高效地完成所有引用的更新操作。雖然這增加了GC的復(fù)雜性,但換取的是運(yùn)行時(shí)更高的訪問(wèn)性能
-
壓縮指針:
- 在64位JVM上,直接指針的大小是64位(8字節(jié)),這相比32位指針會(huì)帶來(lái)更大的內(nèi)存占用和帶寬消耗
- HotSpot引入了壓縮指針技術(shù)(-XX:+UseCompressedOops,默認(rèn)開(kāi)啟)
- 原理:并非真的用64位地址去尋址,而是通過(guò)一定的偏移和縮放,將64位的地址用32位的數(shù)據(jù)來(lái)存儲(chǔ)和表示。JVM在運(yùn)行時(shí)會(huì)將這32位的"壓縮指針"左移3位(相當(dāng)于乘以8)再加上一個(gè)基地址,來(lái)得到真正的64位地址
- 效果:這讓引用變量的大小從8字節(jié)降到了4字節(jié),節(jié)省了大量?jī)?nèi)存(尤其是大量小對(duì)象時(shí)),同時(shí)因?yàn)镃PU緩存能容納更多引用,也間接提升了訪問(wèn)速度
3.4 對(duì)比總結(jié)
| 特性 | 句柄訪問(wèn) | 直接指針訪問(wèn) (HotSpot) |
|---|---|---|
| reference存儲(chǔ)內(nèi)容 | 句柄的地址 | 對(duì)象的直接地址 |
| 訪問(wèn)開(kāi)銷(xiāo) | 兩次指針尋址(速度慢) | 一次指針尋址(速度快) |
| GC時(shí)引用更新 | 對(duì)象移動(dòng)時(shí),只需更新句柄,reference不變 | 對(duì)象移動(dòng)時(shí),必須更新reference |
| 內(nèi)存占用 | 需要額外句柄池空間 | 對(duì)象頭中需要類(lèi)型指針,但總體更節(jié)省 |
| 優(yōu)點(diǎn) | 引用穩(wěn)定,利于GC | 性能極高,訪問(wèn)速度快 |
| 缺點(diǎn) | 訪問(wèn)速度慢,占用額外內(nèi)存 | GC時(shí)更新引用的開(kāi)銷(xiāo)更大 |
四、總結(jié)
Java對(duì)象的創(chuàng)建、內(nèi)存布局和訪問(wèn)定位是JVM的核心機(jī)制。通過(guò)本文的詳細(xì)解析,我們可以看到,一個(gè)簡(jiǎn)單的new操作背后,JVM進(jìn)行了類(lèi)加載檢查、內(nèi)存分配、初始化、設(shè)置對(duì)象頭和執(zhí)行構(gòu)造方法等一系列復(fù)雜操作。
對(duì)象在內(nèi)存中的布局分為對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充三部分,其中對(duì)象頭包含了Mark Word和類(lèi)型指針等關(guān)鍵信息,用于JVM管理對(duì)象的生命周期、同步狀態(tài)和類(lèi)型識(shí)別。
HotSpot VM為了追求極致的執(zhí)行性能,選擇了直接指針作為對(duì)象訪問(wèn)定位的方式。它通過(guò)投入巨大的工程復(fù)雜度在垃圾收集器上(如精確式GC、卡表、讀屏障等技術(shù))來(lái)克服直接指針在對(duì)象移動(dòng)時(shí)的缺點(diǎn),從而最終贏得了性能上的優(yōu)勢(shì)。
理解這些底層機(jī)制,不僅能夠幫助我們編寫(xiě)更高效的Java代碼,還能在進(jìn)行性能調(diào)優(yōu)、內(nèi)存分析和故障排查時(shí)提供重要的理論依據(jù)和實(shí)踐指導(dǎo)。
附錄
相關(guān)JVM參數(shù)
-XX:+UseTLAB:?jiǎn)⒂镁€程本地分配緩沖(默認(rèn)開(kāi)啟)-XX:+UseCompressedOops:?jiǎn)⒂脡嚎s指針(64位JVM默認(rèn)開(kāi)啟)-XX:CompactFields:允許子類(lèi)窄變量插入父類(lèi)變量空隙(默認(rèn)開(kāi)啟)-XX:MaxTenuringThreshold:設(shè)置對(duì)象晉升老年代的年齡閾值(默認(rèn)15)
推薦工具
- JOL(Java Object Layout):分析對(duì)象內(nèi)存布局
- HSDB(HotSpot Debugger):深入分析JVM運(yùn)行時(shí)狀態(tài)
- JMC(Java Mission Control):監(jiān)控和分析JVM運(yùn)行性能
參考資料
- 《深入理解Java虛擬機(jī)》 - 周志明
- OpenJDK官方文檔
- Java語(yǔ)言規(guī)范(JLS)
- JVM規(guī)范(JVMS)
- Oracle官方Java性能調(diào)優(yōu)指南
?? 如果你喜歡這篇文章,請(qǐng)點(diǎn)贊支持! ?? 同時(shí)歡迎關(guān)注我的博客,獲取更多精彩內(nèi)容!
本文來(lái)自博客園,作者:佛祖讓我來(lái)巡山,轉(zhuǎn)載請(qǐng)注明原文鏈接:http://www.rzrgm.cn/sun-10387834/p/19086078

浙公網(wǎng)安備 33010602011771號(hào)