JVM-對象及對象內存布局
前言
上一篇文章對JVM的運行時數據區域的內容進行了梳理,本篇文章對JVM中的對象和對象的內存布局進行深入解析。本文參考了《深入理解Java虛擬機》、《深入解析Java虛擬機HotSpot》、《HotSpot實戰》三本書。
下面提到的虛擬機都特指JDK1.8版本的HotSpot VM,其他虛擬機的實現有可能不太一樣。
類與對象
在編譯時,通過Javac編譯器為虛擬機規範的class文件格式。class文件格式是與操作系統和機器指令集無關的、平台中立的格式。其他語言編寫的代碼只需要實現指定語言的編譯器編譯位JVM規範標準的class文件就可以實現該語言運行在JVM之上,這就是JVM的語言無關性。
通過java命令運行class文件,首先會通過類加載器將class文件加載到內存中,加載class文件會為類生成一個klass實例。在klass包含了用於描述Java類的元數據,包括字段個數、大小、是否為數組、是否有父類、方法信息等。
對象類二分模型
HotSpot虛擬機是使用C++實現的, C++也是面向對象語言。可以採用java類一一映射到C++類,當創建Java對象就創建對應的C++類的對象。
但是由於如果C++的對象含有虛函數,則創建的對象會有虛方法表指針,指向虛方法表。如果採用這種直接一對一映射的方式,會導致含有虛方法的類創建的對象都包含虛方法指針。因此在HotSpot虛擬機中,通過對象類二分模型,將類描述信息和實例數據進行拆分。使用僅包含數據不包含方法的oop(Ordinary Object Pointer)對象描述Java的對象,使用klass描述java的類。oop的職責在於表示對象實例數據,沒必要維護虛函數指針。通過oop對象頭部的一個指針指向對應的klass對象進行關聯。
在HotSpot虛擬機中,普通對象的類通過instanceKlass表示,對象實例則通過instanceOopDesc表示。
在JVM中引用類型可以分為對象,基本類型數組和對象類型數組。可以分別映射到Java中的對應的對象和類型。
類 | 對象 | |
---|---|---|
對象 | instanceKlass | instanceOopDesc |
基本類型數組 | typeArrayKlass | typeArrayOopDesc |
對象類型數組 | objArrayKlass | objArrayOopDesc |
除了常用的3類引用對象外,還有一些其他JVM自己要用的java.lang.ClassLoader
用InstanceClassLoaderKlass
描述,java.lang.Class
用InstanceMirrorKlass
描述等。
對象
HotSpot VM使用oop描述對象,oop字面意思是「普通對象指針」。它是指向一片內存的指針,只是將這片內存『視作』(強制類型轉換)Java對象/數組。對象的本質就是用對象頭和字段數據填充這片內存。
對象內存布局
JOL工具
在談論具體對象布局時,推薦一個JOL工具,可以打印對象的內存布局。通過maven引入。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
通過ClassLayout.parseInstance(new Object()).toPrintable()
即可打印對象的內存布局。
對象頭
普通對象的對象頭包含2部分,第一部分被稱為Mark Word
,第二部分為類型指針。如果對象為數組,除了普通對象的兩部分外對象頭還包含數組長度。下圖是64位虛擬機對象頭。
32位虛擬機頭部的Mark Word長度為4個位元組。
Mark Word
Mark Word
保存了對象運行時必要的信息,包括哈希碼(HashCode)、GC分代年齡、偏向狀態、鎖狀態標誌、偏向線程ID、偏向時間戳等信息。通過類型指針,可以找到對象對應的類型信息。32位虛擬機和64位虛擬機的Mark Word
長度分別為4位元組和8位元組。
不論是32位還是64位虛擬機的對象頭部都使用了4比特記錄分代年齡,每次GC時對象倖存年齡都會加1,因此對象在survivor區最多倖存15次,超過15次時,仍然有可達根的對象就會從survivor區被轉移到老年代。可以通過-XX:MaxTenuringThreshold=15
參數修改最大倖存年齡。
CMS垃圾回收器默認為6次。
類型句柄
相比32位對象頭大小,64位對象頭更大一些,64位虛擬機對象頭的Mark Word
和類型指針地址
都是8位元組。而通常情況,我們的程序不需要佔用那麼大的內存。因此虛擬機通過壓縮指針功能,將對象頭的類型指針進行壓縮。而Mark Word
由於運行時需要保存的頭部信息會大於4位元組,仍然使用8位元組。若配置開啟了-XX:+UseCompressedOops
,虛擬機會將類型指針地址
壓縮為32位。若配置開啟了-XX:+UseCompressedClassPointers
,則會壓縮klass對象的地址為32位。
需要注意的是,當地址經過壓縮後,尋址範圍不可避免的會降低。對於64位CPU,由於目前內存一般到不了2^64,因此大多數64位CPU的地址總線實際會小於64位,比如48位。
開啟-XX:+UseCompressedOops
,默認也會開啟-XX:+UseCompressedClassPointers
。關閉-XX:+UseCompressedOops
,默認也會關閉-XX:+UseCompressedClassPointers
。
如果開啟-XX:+UseCompressedOops
,但是關閉-XX:+UseCompressedClassPointers
,啟動虛擬機的時候會提示「Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops」。
普通對象內存布局(64位虛擬機指針壓縮時)
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) Mark Word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 類型指針
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
需要注意,由於內存按小端模式分佈,因此顯示的內容是反着的。上面實際對象頭內容為 00000000 00000001 f80001e5
數組對象內存布局(64位虛擬機指針壓縮時)
[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) Mark Word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779) 類型指針
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 數組長度
16 4 java.lang.Object Object;.<elements> N/A 數組元素
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
對象頭與鎖膨脹
對象頭中存儲了鎖的必要信息,不同的鎖的對象頭存儲內容稍有不同。32位對象頭存儲格式如下
JVM底層對加鎖進行了性能優化,默認虛擬機啟動後大約4秒會開啟偏向鎖功能。當虛擬機未啟用偏向鎖時,鎖的演化過程為無鎖->輕量鎖(自旋鎖)->重量鎖
。
當虛擬機啟用了偏向鎖時,鎖的演化過程為無鎖->偏向鎖->輕量鎖(自旋鎖)->重量鎖
。
本文不討論JVM對加鎖的具體優化邏輯,內容比較多,感興趣的可以看同學可以參考《淺談偏向鎖、輕量級鎖、重量級鎖》。
無鎖
當對象未加鎖時,鎖狀態為01
,32位虛擬機的對象頭部如圖所示
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
/*java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total*/
}
需要注意的是其中對象頭保存的hashCode
被稱為identityHashCode
,當我們調用對象的hashCode
方法,返回的就是該值。若我們重寫了hashCode
的值,對象頭的hashCode
值仍然是內部的identityHashCode
,而不是我們重寫的hashCode
值。可以通過System.identityHashCode
打印identityHashCode
,或者也可以通過toString
直接打印對象輸出16進制的identityHashCode
。
public static void main(String[] args){
Object obj1 = new Object();
System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
System.out.println(obj1.hashCode());
System.out.println(System.identityHashCode(obj1));
System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
System.out.println(obj1);
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
1766822961
1766822961
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 31 94 4f (00000001 00110001 10010100 01001111) (1335111937)
4 4 (object header) 69 00 00 00 (01101001 00000000 00000000 00000000) (105)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object@694f9431 十進制即為1766822961,二進制為01101001 01001111 10010100 00110001
*/
}
偏向鎖
偏向鎖中的「偏」,就是偏心的「偏」、偏袒的「偏」。它的意思是這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖一直沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
偏向鎖的鎖狀態和未鎖狀態一樣都是01
,當對象處於偏向狀態時,偏向標記為1
;當對象處於未偏向時,偏向標記為0
。
32位虛擬機的偏向鎖對象頭部如圖所示
偏向時間戳,它實際表示偏向的有效期。
無鎖狀態升級為偏向鎖的條件:
- 對象可偏向,對象未加鎖時,執行CAS更新對象頭部線程偏向線程ID為當前線程成功。
- 對象可偏向,對象已加鎖,但偏向線程ID為空,執行CAS更新對象頭部線程偏向線程ID為當前線程成功。
- 對象可偏向,對象已加鎖,且偏向線程ID等於當前線程ID。
- 對象可偏向,對象已加鎖,且偏向線程ID不為空且不等於當前線程ID,執行CAS更新對象頭部線程偏向線程ID為當前線程成功。
虛擬機啟動時,會根據-XX:BiasedLockingStartupDelay
配置延遲啟動偏向,在JDK1.8中,默認為4秒。有需要時可以通過-XX:BiasedLockingStartupDelay=0
關閉延時偏向。
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 9c 02 (00000101 00101000 10011100 00000010) (43788293)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
}
輕量級鎖
輕量級鎖的鎖狀態為00
,32位虛擬機的輕量級鎖頭部格式如下
升級為輕量級鎖條件:
- 對象不可偏向,跳過偏向鎖直接使用輕量級鎖。
- 對象可偏向,但偏向加鎖失敗(存在線程競爭)。
- 對象獲取調用
hashCode
後加鎖。 - 對象已升級為重量級鎖後,鎖降級只能降級為輕量級鎖,無法降級為偏向鎖。
輕量級鎖會在線程的棧幀中開闢一個鎖記錄區域,將當前對象的頭部保存在鎖記錄區域中,將鎖記錄區域的地址保存到當前對象頭部。
- 對象不可偏向直接升級到輕量鎖
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 58 f4 dd 02 (01011000 11110100 11011101 00000010) (48100440)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
- 偏向鎖競爭升級為輕量鎖
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向鎖
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e8 31 28 (00000101 11101000 00110001 00101000) (674359301)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
}
};
thread.start();
thread.join();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //輕量鎖
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f4 b4 02 (01100000 11110100 10110100 00000010) (45413472)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
- 偏向後調用
hashCode
方法升級為輕量級鎖
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
final Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向鎖
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
o.hashCode();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 18 f3 4a 02 (00011000 11110011 01001010 00000010) (38466328)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
重量級鎖
輕量級鎖的鎖狀態為10
,32位重量級鎖頭部如圖所示
輕量級鎖自循環一定次數後一致獲取不到鎖,則升級為重量級鎖條件。自旋次數默認為10次,可以通過-XX:PreBlockSpin
配置修改次數。
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
};
thread.start();
synchronized (o){
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
thread.join();
}
重量級鎖降級
當重量級鎖解鎖後就會進行鎖降級,鎖降級只能降級為輕量鎖,無法再使用偏向鎖。
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
};
thread.start();
synchronized (o){
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
thread.join();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f6 8c 02 (10011000 11110110 10001100 00000010) (42792600)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
實例數據
對象實例數據默認按照long、double、int、short、char、byte、boolean、reference
順序布局,相同字段寬度總是分配在一起。若有父對象,則父對象的實例字段在子對象前面。
另外如果HotSpot虛擬機的 +XX:CompactFields
參數值為true(默認就為true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節省出一點點空間。
填充
JVM中,對象大小默認為8的整數倍,若對象大小不能被8整除,則會填充空位元組來填充對象保證。
對象生命周期
在了解完對象頭部後,我們看下對象的創建的時候發生了什麼事情。當我們調用new Object()
創建一個對象時,生成的位元組碼如下
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
首先通過new
指令分配對象,並將對象地址入棧,通過dup
指令複製一份棧頂元素。通過invokespecial
指令調用對象的init
進行初始化會消耗棧頂2個槽。由於init
方法需要傳入一個參數,該參數即為引用對象本身。在init初始化時會將this
指針進行賦值。這樣我們在代碼中就可以通過this
指向當前對象。
對象創建流程如下圖所示。
-
棧上分配
通常對象都是在堆上創建的,若對象僅在當前作用域下使用,那麼使用完很快就會被GC回收。JVM通過逃逸分析對對象作用域進行分析,如果對象僅在當前作用域下使用,則將對象的實例數據分配在棧上,從而提升對象創建速度的同時減少GC回收的對象數量。 -
線程局部緩衝區(TLAB)
如果無法在棧上分配,則對象會在堆上分配。對於JDK1.8來說,Java堆通常使用分代模型,(關於GC,垃圾回收算法等這裡不做具體討論)。經過統計,90%的對象在使用完成後都會被回收,因此默認新生代會分配10%的空間給倖存者區。
對象先在eden區進行分配,但是我們知道,堆是所有線程共享的區域,會存在多線程並發問題。因此在堆上分配就需要進行線程同步。為了提高分配效率,JVM會為每個線程從eden區初始化一塊堆內存,該內存是線程私有的。這樣每次分配對象時就無需進行同步操作,從而提高對象分配效率。線程的這塊局部內存區域被稱為線程局部緩衝區(TLAB)。通常這塊內存會小於eden區的1%。當這塊內存用完時,就會重新通過CAS的方式為線程重新分配一塊TLAB。
通常對象分配有兩種方式,一種是線性分配,當內存是規整時(大部分垃圾回收器新生代都是用標記清理算法,可以保證內存規整),通過一個指針向後移動對象大小,直接分配一塊內存給對象,指針左邊是已使用的內存,指針右邊是未使用的內存,這種方式被稱為指針碰撞。TLAB配合指針碰撞技術能夠在線程安全的情況下移動一次指針直接就可以完成對象的內存分配。
當內存不規整時(比如CMS垃圾回收器通常情況並不會每次GC後都壓縮內存,會存在內存碎片),則需要一塊額外的內存記錄哪些內存是空閑的,這個緩存被稱為空閑列表。
-
eden區分配
如果TLAB無法分配對象,那麼對象只能在Eden區直接分配,前面說過,在堆上分配,必須採用同步策略避免有產生線程安全問題。如果分配內存時,對象的klass沒有解析過,則需要先進行類加載過程,然後才能分配對象。這個過程被稱為慢速分配,而如果klass已解析過則直接可以分配對象,這個過程被稱為快速分配。 -
老年代分配
當eden區放不下對象時(當然還有其他的判斷策略,這裡暫時不去關心),對象直接分配到老年代。 -
對象實例初始化
當對象完成內存分配時,就會初始化對象,將內存清零。需要注意,對象的靜態變量在類初始化的初始化階段已經完成設置。 -
初始化對象頭部
當對象實例初始化完,就會設置對象頭部,默認的對象頭部存放在klass,如果啟用了偏向,則設置的就是可偏向的對象頭。
對象訪問方式
現在我們了解了對象的內存布局和對象的創建邏輯,那麼對象在運行時,如何通過棧的局部變量找到實際的對象呢?常用的對象訪問方式有2種,直接指針訪問和句柄訪問。
直接指針訪問
對象創建時,局部變量表只保存對象的地址,地址指向的是堆中的實際對象的markword地址,JVM中採用的就是這種方式訪問對象。
句柄訪問
通過句柄訪問時局部變量保存的時句柄池的對象句柄,句柄池中,則會存儲對象實例指針和對象類型指針。再通過這兩個指針分別指向對象實例池中的對象和元數據的klass。
相比直接指針訪問,這種訪問方式由於需要2次訪問,而直接指針只需要一次訪問,因此句柄訪問對象的速度相對較慢。但是對於垃圾回收器來說是比較友好的,因為對象移動無需更新棧中的局部變量表的內容,只需要更新句柄池中的對象實例指針的值。
HSDB
前面我們通過JOL工具可以很方便的輸出對象的布局。JDK也提供了一些工具可以查看更詳細的運行時數據。
HSDB(Hotspot Debugger) 是 JDK1.8 自帶的工具,使用該工具可以連接到運行時的java進程,查看到JVM運行時的狀態。
以該偏向鎖代碼為例
//-XX:BiasedLockingStartupDelay=0
public class BiasedLock {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
/*
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 64 03 (00000101 00101000 01100100 00000011) (56895493)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/
為了能看到運行時狀態,我們可以使用idea工具單筆調試,也可以使用jdb工具進行調試。jdb是Java的調試器,位於%JAVA_HOME%/bin
下面。 通過jdb -classpath XXX class名
執行main方法。
執行後,我們可以將打斷點,然後進行調試。
- 通過
stop in <class id>.<method>[(argument_type,...)]
在方法中打斷點,或者可以通過stop at <class id>:<line>
在指定行打斷點。 - 通過
stop in com.company.BiasedLock.main
將斷點打在main方法。 - 通過
run
運行 - 通過next進行調試。(可以使用step進行單步調試)
C:\Users\Dm_ca>jdb -classpath "D:\study\java\symbolreference\target\classes;D:\develop\mavenrepository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar" com.company.lock.BiasedLock
正在初始化jdb...
> stop in com.company.lock.BiasedLock.main
正在延遲斷點com.company.lock.BiasedLock.main。
將在加載類後設置。
> run
運行com.company.lock.BiasedLock
設置未捕獲的java.lang.Throwable
設置延遲的未捕獲的java.lang.Throwable
>
VM 已啟動: 設置延遲的斷點com.company.lock.BiasedLock.main
斷點命中: "線程=main", com.company.lock.BiasedLock.main(), 行=8 bci=0
main[1] next
>
已完成的步驟: "線程=main", com.company.lock.BiasedLock.main(), 行=9 bci=8
...
未掛起任何對象。
> java.lang.Object o
bject internals:
OFFSET已完成的步驟: SIZE TYPE DESCRIPTION VALUE
"線程=main", com.company.lock.BiasedLock.main(), 行=11 bci=25
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
此時我們可以通過HSDB連接到進程。通過JPS
命令查看進程的pid
C:\Users\Dm_ca>jps
17072
22724 HSDB
23268 TTY
24548 Launcher
3828 BiasedLock
通過java -cp 「.;%JAVA_HOME%/lib/sa-jdi.jar」 sun.jvm.hotspot.HSDB 3828
啟動HSDB(這種方式會阻塞我們的程序,不要直接在生產環境這樣操作)
第一次啟動可能會報錯誤無法找到sawindbg.dll
,這時需要將%JAVA_HOME%/lib
目錄下面的sawindbg.dll
文件拷貝到jre的/lib
目錄下即可。
啟動後,在界面選中main線程,點擊工具欄第二個圖片打開線程棧。
HSDB工具在線程棧中已經標出我們的對象。在菜單找到內存查看器
輸入棧局部變量表中的對象的地址,就可以顯示出對象的內存,和JOL工具打印的對象頭部是一樣的。
參考文檔
- HSDB – HotSpot debugger
- JOL:分析Java對象的內存布局
- [Java JVM] Hotspot GC研究- 開篇&對象內存布局
- 看了這篇文章,我搞懂了StringTable
- 盤一盤 synchronized (一)—— 從打印Java對象頭說起
- 淺談偏向鎖、輕量級鎖、重量級鎖
- 源碼解析-線程A請求偏向於線程B的偏向鎖
- C++為什麼要弄出虛表這個東西?
- 《深入理解Java虛擬機》
- 《Java虛擬機規範(Java SE 8版)》
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處://www.cnblogs.com/Jack-Blog/p/14481982.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及鏈接。