對於 JVM
運行時區域有了必定了解之後,本文將更進一步介紹虛擬機內存中的數據的細節信息。以JVM
虛擬機(Hotspot
)的內存區域Java
堆爲例,探討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;
}
}
複製代碼
這是最多見也是最簡單的建立對象的方式了。經過這種方式,咱們能夠調用任意的構造函數(無參的和帶參數的)。緩存
Employee emp1 = new Employee();
複製代碼
Employee emp1 = new Employee(name);
複製代碼
咱們也可使用Class
類的newInstance
方法建立對象。這個newInstance
方法調用無參的構造函數建立對象。多線程
Employee emp2 = (Employee) Class.forName("org.ostenant.jvm.instance.Employee").newInstance();
複製代碼
Employee emp2 = Employee.class.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方法。框架
不管什麼時候咱們調用一個對象的clone
方法,JVM
都會建立一個新的對象,將前面對象的內容所有拷貝進去。用clone
方法建立對象並不會調用任何構造函數。異步
爲了使用clone
方法,咱們須要先實現Cloneable
接口並實現其定義的clone
方法。jvm
Employee emp4 = (Employee) emp3.clone();
複製代碼
當咱們序列化和反序列化一個對象,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
虛擬機的實現。目前主流的訪問方式有 句柄 和 直接指針 兩種方式。
指針: 指向對象,表明一個對象在內存中的起始地址。 句柄: 能夠理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。
Java
堆中劃分出一塊內存來做爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據與對象類型數據各自的具體地址信息,具體構造以下圖所示:
優點:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而引用自己不須要修改。
若是使用直接指針訪問,引用 中存儲的直接就是對象地址,那麼Java
堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。
優點:速度更快,節省了一次指針定位的時間開銷。因爲對象的訪問在Java
中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。
周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。