第 10 章 對象的執行個體化記憶體布局與通路定位
1、對象的執行個體化
大廠面試題
美團:
- 對象在
中是怎麼存儲的?JVM
- 對象頭資訊裡面有哪些東西?
螞蟻金服:
二面:
java
對象頭裡有什麼
對象執行個體化
1.1、對象建立的方式
對象建立的方式
- new:最常見的方式、單例類中調用getInstance的靜态類方法,XXXFactory的靜态方法
- Class的newInstance方法:在JDK9裡面被标記為過時的方法,因為隻能調用空參構造器,并且權限必須為 public
- Constructor的newInstance(Xxxx):反射的方式,可以調用空參的,或者帶參的構造器
- 使用clone():不調用任何的構造器,要求目前的類需要實作Cloneable接口中的clone方法
- 使用序列化:序列化一般用于Socket的網絡傳輸
- 第三方庫 Objenesis
1.2、對象建立的步驟
從位元組碼看待對象的建立過程
- 代碼
/**
* @author shkstart [email protected]
* @create 2020 17:16
*/
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
複制
- main() 方法對應的位元組碼(後面細講):
- 調用 new 指令後後,加載 Object 類
- 調用 Object 類的 init() 方法
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return
複制
建立對象的步驟
1、判斷對象對應的類是否加載、連結、初始化
- 虛拟機遇到一條new指令,首先去檢查這個指令的參數能否在Metaspace的常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已經被加載,解析和初始化。(即判斷類元資訊是否存在)。
- 如果該類沒有加載,那麼在雙親委派模式下,使用目前類加載器以ClassLoader + 包名 + 類名為key進行查找對應的.class檔案,如果沒有找到檔案,則抛出ClassNotFoundException異常,如果找到,則進行類加載,并生成對應的Class對象。
2、為對象配置設定記憶體
- 首先計算對象占用空間的大小,接着在堆中劃分一塊記憶體給新對象。如果執行個體成員變量是引用變量,僅配置設定引用變量空間即可,即4個位元組大小
- 如果記憶體規整:采用指針碰撞配置設定記憶體
- 如果記憶體是規整的,那麼虛拟機将采用的是指針碰撞法(Bump The Point)來為對象配置設定記憶體。
- 意思是所有用過的記憶體在一邊,空閑的記憶體放另外一邊,中間放着一個指針作為分界點的訓示器,配置設定記憶體就僅僅是把指針往空閑記憶體那邊挪動一段與對象大小相等的距離罷了。
- 如果垃圾收集器選擇的是Serial ,ParNew這種基于壓縮算法的,虛拟機采用這種配置設定方式。一般使用帶Compact(整理)過程的收集器時,使用指針碰撞。
- 标記壓縮(整理)算法會整理記憶體碎片,堆記憶體一存對象,另一邊為空閑區域
- 如果記憶體不規整
- 如果記憶體不是規整的,已使用的記憶體和未使用的記憶體互相交錯,那麼虛拟機将采用的是空閑清單來為對象配置設定記憶體。
- 意思是虛拟機維護了一個清單,記錄上哪些記憶體塊是可用的,再配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的内容。這種配置設定方式成為了 “空閑清單(Free List)”
- 選擇哪種配置設定方式由Java堆是否規整所決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定
- 标記清除算法清理過後的堆記憶體,就會存在很多記憶體碎片。
3、處理并發問題
- 采用CAS+失敗重試保證更新的原子性
- 每個線程預先配置設定TLAB - 通過設定 -XX:+UseTLAB參數來設定(區域加鎖機制)
- 在Eden區給每個線程配置設定一塊區域
4、初始化配置設定到的記憶體
所有屬性設定預設值,保證對象執行個體字段在不指派可以直接使用
5、設定對象的對象頭
将對象的所屬類(即類的中繼資料資訊)、對象的HashCode和對象的GC資訊、鎖資訊等資料存儲在對象的對象頭中。這個過程的具體設定方式取決于JVM實作。
6、執行init方法進行初始化
- 在Java程式的視角看來,初始化才正式開始。初始化成員變量,執行執行個體化代碼塊,調用類的構造方法,并把堆内對象的首位址指派給引用變量
- 是以一般來說(由位元組碼中跟随invokespecial指令所決定),new指令之後會接着就是執行init方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完成建立出來。
回顧給對象屬性指派的順序:
- 屬性的預設值初始化
- 顯示初始化/代碼塊初始化(并列關系,誰先誰後看代碼編寫的順序)
- 構造器初始化
從位元組碼角度看 init 方法
- 代碼
/**
* 測試對象執行個體化的過程
* ① 加載類元資訊 - ② 為對象配置設定記憶體 - ③ 處理并發問題 - ④ 屬性的預設初始化(零值初始化)
* - ⑤ 設定對象頭的資訊 - ⑥ 屬性的顯式初始化、代碼塊中初始化、構造器中初始化
*
*
* 給對象的屬性指派的操作:
* ① 屬性的預設初始化 - ② 顯式初始化 / ③ 代碼塊中初始化 - ④ 構造器中初始化
* @author shkstart [email protected]
* @create 2020 17:58
*/
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
複制
- init() 方法的位元組碼指令:
- 屬性的預設值初始化:
id = 1001;
- 顯示初始化/代碼塊初始化:
name = "匿名客戶";
- 構造器初始化:
acct = new Account();
- 屬性的預設值初始化:
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 sipush 1001
8 putfield #2 <com/atguigu/java/Customer.id>
11 aload_0
12 ldc #3 <匿名客戶>
14 putfield #4 <com/atguigu/java/Customer.name>
17 aload_0
18 new #5 <com/atguigu/java/Account>
21 dup
22 invokespecial #6 <com/atguigu/java/Account.<init>>
25 putfield #7 <com/atguigu/java/Customer.acct>
28 return
複制
2、對象的記憶體布局
對象記憶體布局
2.1、對象頭
對象頭
對象頭包含兩部分:運作時中繼資料(Mark Word)和類型指針
- 運作時中繼資料
- 哈希值(HashCode),可以看作是堆中對象的位址
- GC分代年齡(年齡計數器)
- 鎖狀态标志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
- 類型指針
- 指向類中繼資料InstanceKlass,确定該對象所屬的類型。指向的其實是方法區中存放的類元資訊
說明:如果對象是數組,還需要記錄數組的長度
2.2、執行個體資料
執行個體資料(Instance Data)
- 說明
- 它是對象真正存儲的有效資訊,包括程式代碼中定義的各種類型的字段(包括從父類繼承下來的和本身擁有的字段)
- 規則
- 相同寬度的字段總是被配置設定在一起
- 父類中定義的變量會出現在子類之前(父類在子類之前加載)
- 如果CompactFields參數為true(預設為true):子類的窄變量可能插入到父類變量的空隙
2.3、對齊填充
對齊填充
不是必須的,也沒特别含義,僅僅起到占位符的作用
記憶體布局總結
- 代碼
/**
* 測試對象執行個體化的過程
* ① 加載類元資訊 - ② 為對象配置設定記憶體 - ③ 處理并發問題 - ④ 屬性的預設初始化(零值初始化)
* - ⑤ 設定對象頭的資訊 - ⑥ 屬性的顯式初始化、代碼塊中初始化、構造器中初始化
*
*
* 給對象的屬性指派的操作:
* ① 屬性的預設初始化 - ② 顯式初始化 / ③ 代碼塊中初始化 - ④ 構造器中初始化
* @author shkstart [email protected]
* @create 2020 17:58
*/
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
/**
* @author shkstart [email protected]
* @create 2020 17:16
*/
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
複制
- 圖解記憶體布局
3、對象的通路定位
JVM是如何通過棧幀中的對象引用通路到其内部的對象執行個體呢?
對象的兩種通路方式:句柄通路和直接指針
1、句柄通路
- 缺點:在堆空間中開辟了一塊空間作為句柄池,句柄池本身也會占用空間;通過兩次指針通路才能通路到堆中的對象,效率低
- 優點:reference中存儲穩定句柄位址,對象被移動(垃圾收集時移動對象很普遍)時隻會改變句柄中執行個體資料指針即可,reference本身不需要被修改
2、直接指針(HotSpot采用)
- 優點:直接指針是局部變量表中的引用,直接指向堆中的執行個體,在對象執行個體中有類型指針,指向的是方法區中的對象類型資料
- 缺點:對象被移動(垃圾收集時移動對象很普遍)時需要修改 reference 的值