第 10 章 物件的實體化記憶體布局與訪問定位
1、物件的實體化
大廠面試題
美團:
- 物件在
JVM中是怎么存盤的? - 物件頭資訊里面有哪些東西?
螞蟻金服:
二面: java物件頭里有什么
物件實體化

1.1、物件創建的方式
物件創建的方式
- new:最常見的方式、單例類中呼叫getInstance的靜態類方法,XXXFactory的靜態方法
- Class的newInstance方法:在JDK9里面被標記為過時的方法,因為只能呼叫空參構造器,并且權限必須為 public
- Constructor的newInstance(Xxxx):反射的方式,可以呼叫空參的,或者帶參的構造器
- 使用clone():不呼叫任何的構造器,要求當前的類需要實作Cloneable介面中的clone方法
- 使用序列化:序列化一般用于Socket的網路傳輸
- 第三方庫 Objenesis
1.2、物件創建的步驟
從位元組碼看待物件的創建程序
- 代碼
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
</java></java>
創建物件的步驟
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 方法
- 代碼
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
</com></com></com></com></匿名客戶></com></java>
2、物件的記憶體布局
物件記憶體布局

2.1、物件頭
物件頭
物件頭包含兩部分:運行時元資料(Mark Word)和型別指標
- 運行時元資料
- 哈希值(HashCode),可以看作是堆中物件的地址
- GC分代年齡(年齡計數器)
- 鎖狀態標志
- 執行緒持有的鎖
- 偏向執行緒ID
- 偏向時間戳
- 型別指標
- 指向類元資料InstanceKlass,確定該物件所屬的型別,指向的其實是方法區中存放的類元資訊
說明:如果物件是陣列,還需要記錄陣列的長度
2.2、實體資料
實體資料(Instance Data)
- 說明
- 它是物件真正存盤的有效資訊,包括程式代碼中定義的各種型別的欄位(包括從父類繼承下來的和本身擁有的欄位)
- 規則
- 相同寬度的欄位總是被分配在一起
- 父類中定義的變數會出現在子類之前(父類在子類之前加載)
- 如果CompactFields引數為true(默認為true):子類的窄變數可能插入到父類變數的空隙
2.3、對齊填充
對齊填充
不是必須的,也沒特別含義,僅僅起到占位符的作用
記憶體布局總結
- 代碼
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
- 圖解記憶體布局

3、物件的訪問定位
JVM是如何通過堆疊幀中的物件參考訪問到其內部的物件實體呢?


物件的兩種訪問方式:句柄訪問和直接指標
1、句柄訪問
- 缺點:在堆空間中開辟了一塊空間作為句柄池,句柄池本身也會占用空間;通過兩次指標訪問才能訪問到堆中的物件,效率低
- 優點:reference中存盤穩定句柄地址,物件被移動(垃圾收集時移動物件很普遍)時只會改變句柄中實體資料指標即可,reference本身不需要被修改

2、直接指標(HotSpot采用)
- 優點:直接指標是區域變數表中的參考,直接指向堆中的實體,在物件實體中有型別指標,指向的是方法區中的物件型別資料
- 缺點:物件被移動(垃圾收集時移動物件很普遍)時需要修改 reference 的值

你只管學習,我來負責記筆記?? 關注公眾號! ,更多筆記,等你來拿,謝謝





轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/168762.html
標籤:Java
上一篇:第 9 章 方法區
