1. 程式人生 > >JVM系列(三) - JVM對象探秘

JVM系列(三) - JVM對象探秘

nsh 常量池 通過 記錄 ray inpu its tac catch

前言

對於 JVM 運行時區域有了一定了解以後,本文將更進一步介紹虛擬機內存中的數據的細節信息。以JVM虛擬機(Hotspot)的內存區域Java堆為例,探討Java堆是如何創建對象、如何布局對象以及如何訪問對象的。

正文

(一). 對象的創建

說到對象的創建,首先讓我們看看 Java 中提供的幾種對象創建方式:

Header解釋
使用new關鍵字 調用了構造函數
使用Class的newInstance方法 調用了構造函數
使用Constructor類的newInstance方法 調用了構造函數
使用clone方法 沒有調用構造函數
使用反序列化 沒有調用構造函數

下面舉例說明五種方式的具體操作方式:

Employee.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Employee implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private String name;

public Employee() {}

public Employee(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}

@Override
public String toString() {
return "Employee [name=" + name + "]";
}

@Override
public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}

1. new關鍵字

這是最常見也是最簡單的創建對象的方式了。通過這種方式,我們可以調用任意的構造函數(無參的和帶參數的)。

1
Employee emp1 = new Employee();
1
Employee emp1 = new Employee(name);

2. Class類的newInstance方法

我們也可以使用Class類的newInstance方法創建對象。這個newInstance方法調用無參的構造函數創建對象。

  • 方式一:

    1
    Employee emp2 = (Employee) Class.forName("org.ostenant.jvm.instance.Employee").newInstance();
  • 方式二:

1
Employee emp2 = Employee.class.newInstance();

3. Constructor類的newInstance方法

Class類的newInstance方法很像, java.lang.reflect.Constructor類裏也有一個newInstance方法可以創建對象。我們可以通過這個newInstance方法調用有參數的和私有構造函數。其中,Constructor可以從對應的Class類中獲得。

1
2
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();

這兩種newInstance方法就是大家所說的反射。事實上Class的newInstance方法內部調用Constructor的newInstance方法。

4. Clone方法

無論何時我們調用一個對象的clone方法,JVM都會創建一個新的對象,將前面對象的內容全部拷貝進去。用clone方法創建對象並不會調用任何構造函數。

為了使用clone方法,我們需要先實現Cloneable接口並實現其定義的clone方法。

1
Employee emp4 = (Employee) emp3.clone();

5. 反序列化

當我們序列化反序列化一個對象,JVM會給我們創建一個單獨的對象。在反序列化時,JVM創建對象並不會調用任何構造函數。

為了反序列化一個對象,我們需要讓我們的類實現Serializable接口。

1
2
3
4
5
6
7
8
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(emp4);

ByteArrayInputStream in = new ByteArrayInputStream(oos.toByteArray());
ObjectInputStream ois =new ObjectInputStream(in);

Employee emp5 = (Employee) in.readObject();


本文以new關鍵字為例,講述JVM堆中對象實例的創建過程如下:

  1. 當虛擬機遇到一條new指令時,首先會檢查這個指令的參數能否在常量池中定位一個符號引用。然後檢查這個符號引用的類字節碼對象是否加載、解析和初始化。如果沒有,將執行對應的類加載過程。

  2. 類加載 完成以後,虛擬機將會為新生對象分配內存區域,對象所需內存空間大小在類加載完成後就已確定。

  3. 內存分配 完成以後,虛擬機將分配到的內存空間都初始化為零值

  4. 虛擬機對對象進行一系列的設置,如所屬類的元信息對象的哈希碼對象GC分帶年齡線程持有的鎖偏向線程ID 等信息。這些信息存儲在對象頭 (Object Header)。

上述工作完成以後,從虛擬機的角度來說,一個新的對象已經產生了。然而,從Java程序的角度來說,對象創建才剛開始。

(二). 對象的布局

HotSpot虛擬機中,對象在內存中存儲的布局可以分為三塊區域:對象頭Header)、實例數據Instance Data)和對齊填充Padding)。

對象頭

HotSpot虛擬機中,對象頭有兩部分信息組成:運行時數據類型指針

1. 運行時數據
用於存儲對象自身運行時的數據,如哈希碼(hashCode)、GC分帶年齡線程持有的鎖偏向線程ID 等信息。

這部分數據的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64Bit,官方稱它為 “Mark Word”

在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32個Bit空間中的25Bit用於存儲對象哈希碼(HashCode),4Bit用於存儲對象分代年齡,2Bits用於存儲鎖標誌位,1Bit固定為0。

在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下表所示:

存儲內容標誌位狀態
對象哈希碼、對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不需要記錄信息 11 GC標記
偏向線程ID、偏向時間戳、對象分代年齡 01 可偏向

2. 類型指針

指向實例對象的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。

實例數據

實例數據 部分是對象真正存儲的有效信息,無論是從父類繼承下來的還是該類自身的,都需要記錄下來,而這部分的存儲順序受虛擬機的分配策略定義的順序的影響。

默認分配策略:

long/double -> int/float -> short/char -> byte/boolean -> reference

如果設置了-XX:FieldsAllocationStyle=0(默認是1),那麽引用類型數據就會優先分配存儲空間:

reference -> long/double -> int/float -> short/char -> byte/boolean

結論:

分配策略總是按照字節大小由大到小的順序排列,相同字節大小的放在一起。

對齊填充

HotSpot虛擬機要求每個對象的起始地址必須是8字節的整數倍,也就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(32位為1倍,64位為2倍),因此,當對象實例數據部分沒有對齊的時候,就需要通過對齊填充來補全。

(三). 對象的訪問定位

Java程序需要通過 JVM 棧上的引用訪問堆中的具體對象。對象的訪問方式取決於 JVM虛擬機的實現。目前主流的訪問方式有 句柄直接指針 兩種方式。

指針: 指向對象,代表一個對象在內存中的起始地址。
句柄: 可以理解為指向指針的指針,維護著對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。

1. 句柄

Java堆中劃分出一塊內存來作為句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據對象類型數據各自的具體地址信息,具體構造如下圖所示:

技術分享圖片

優勢:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中實例數據指針,而引用本身不需要修改。

2. 直接指針

如果使用直接指針訪問,引用 中存儲的直接就是對象地址,那麽Java堆對象內部的布局中就必須考慮如何放置訪問類型數據的相關信息。

技術分享圖片

優勢:速度更,節省了一次指針定位的時間開銷。由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是非常可觀的執行成本。

參考

周誌明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社


歡迎關註技術公眾號: 零壹技術棧

技術分享圖片

零壹技術棧

本帳號將持續分享後端技術幹貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務,架構學習和進階等學習資料和文章。

JVM系列(三) - JVM對象探秘