前言
對於 JVM
執行時區域有了一定了解以後,本文將更進一步介紹虛擬機器記憶體中的資料的細節資訊。以JVM
虛擬機器(Hotspot
)的記憶體區域Java
堆為例,探討Java
堆是如何建立物件、如何佈局物件以及如何訪問物件的。
正文
(一). 物件的建立
說到物件的建立,首先讓我們看看 Java
中提供的幾種物件建立方式:
Header | 解釋 |
---|---|
使用new關鍵字 | 呼叫了建構函式 |
使用Class的newInstance方法 | 呼叫了建構函式 |
使用Constructor類的newInstance方法 | 呼叫了建構函式 |
使用clone方法 | 沒有呼叫建構函式 |
使用反序列化 | 沒有呼叫建構函式 |
下面舉例說明五種方式的具體操作方式:
Employee.java
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關鍵字
這是最常見也是最簡單的建立物件的方式了。通過這種方式,我們可以呼叫任意的建構函式(無參的和帶引數的)。
Employee emp1 = new Employee();
複製程式碼
Employee emp1 = new Employee(name);
複製程式碼
2. Class類的newInstance方法
我們也可以使用Class
類的newInstance
方法建立物件。這個newInstance
方法呼叫無參的建構函式建立物件。
- 方式一:
Employee emp2 = (Employee) Class.forName("org.ostenant.jvm.instance.Employee").newInstance();
複製程式碼
- 方式二:
Employee emp2 = Employee.class.newInstance();
複製程式碼
3. Constructor類的newInstance方法
和Class
類的newInstance
方法很像, java.lang.reflect.Constructor
類裡也有一個newInstance
方法可以建立物件。我們可以通過這個newInstance
方法呼叫有引數的和私有的建構函式。其中,Constructor
可以從對應的Class
類中獲得。
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
複製程式碼
這兩種newInstance方法就是大家所說的反射。事實上Class的newInstance方法內部呼叫Constructor的newInstance方法。
4. Clone方法
無論何時我們呼叫一個物件的clone
方法,JVM
都會建立一個新的物件,將前面物件的內容全部拷貝進去。用clone
方法建立物件並不會呼叫任何建構函式。
為了使用clone
方法,我們需要先實現Cloneable
介面並實現其定義的clone
方法。
Employee emp4 = (Employee) emp3.clone();
複製程式碼
5. 反序列化
當我們序列化和反序列化一個物件,JVM
會給我們建立一個單獨的物件。在反序列化時,JVM
建立物件並不會呼叫任何建構函式。
為了反序列化一個物件,我們需要讓我們的類實現Serializable
介面。
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
堆中物件例項的建立過程如下:
-
當虛擬機器遇到一條
new
指令時,首先會檢查這個指令的引數能否在常量池中定位一個符號引用。然後檢查這個符號引用的類位元組碼物件是否載入、解析和初始化。如果沒有,將執行對應的類載入過程。 -
類載入 完成以後,虛擬機器將會為新生物件分配記憶體區域,物件所需記憶體空間大小在類載入完成後就已確定。
-
記憶體分配 完成以後,虛擬機器將分配到的記憶體空間都初始化為零值。
-
虛擬機器對物件進行一系列的設定,如所屬類的元資訊、物件的雜湊碼、物件GC分帶年齡 、執行緒持有的鎖 、偏向執行緒ID 等資訊。這些資訊儲存在物件頭 (
Object Header
)。
上述工作完成以後,從虛擬機器的角度來說,一個新的物件已經產生了。然而,從Java
程式的角度來說,物件建立才剛開始。
## (二). 物件的佈局
HotSpot
虛擬機器中,物件在記憶體中儲存的佈局可以分為三塊區域:物件頭(Header
)、例項資料(Instance Data
)和對齊填充(Padding
)。
物件頭
在HotSpot
虛擬機器中,物件頭有兩部分資訊組成:執行時資料 和 型別指標。
1. 執行時資料 用於儲存物件自身執行時的資料,如雜湊碼(hashCode)、GC分帶年齡、執行緒持有的鎖、偏向執行緒ID 等資訊。
這部分資料的長度在32
位和64
位的虛擬機器(暫不考慮開啟壓縮指標的場景)中分別為32
個和64
個Bit
,官方稱它為 “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高階特性與最佳實踐,機械工業出版社
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。