物件例項化
參考資料:尚矽谷JVM教程
8.1 物件例項化
面試題
美團:
物件在JVM中是怎麼儲存的?
物件頭資訊裡面有哪些東西?
螞蟻金服:
Java物件頭有什麼?
8.1.1 建立物件的方式
-
new:最常見的方式、Xxx的靜態方法,XxxBuilder/XxxFactory的靜態方法
-
Class的newInstance方法:反射的方式,只能呼叫空參的構造器,許可權必須是public
-
Constructor的newInstance(XXX):反射的方式,可以呼叫空參、帶參的構造器,許可權沒有要求
-
使用clone():不呼叫任何的構造器,要求當前的類需要實現Cloneable介面,實現clone()
-
使用序列化:從檔案中、從網路中獲取一個物件的二進位制流
-
第三方庫 Objenesis
8.1.2. 建立物件的步驟
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
前面所述是從位元組碼角度看待物件的建立過程,現在從執行步驟的角度來分析:
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
引數來設定
4. 初始化分配到的記憶體
所有屬性設定預設值,保證物件例項欄位在不賦值時可以直接使用
5. 設定物件的物件頭
將物件的所屬類(即類的元資料資訊)、物件的HashCode和物件的GC資訊、鎖資訊等資料儲存在物件的物件頭中。這個過程的具體設定方式取決於JVM實現。
6. 執行init方法進行初始化
在Java程式的視角看來,初始化才正式開始。初始化成員變數,執行例項化程式碼塊,呼叫類的構造方法,並把堆內物件的首地址賦值給引用變數。
因此一般來說(由位元組碼中跟隨invokespecial指令所決定),new指令之後會接著就是執行方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完成創建出來。
給物件屬性賦值的操作
-
屬性的預設初始化
-
顯式初始化
-
程式碼塊中初始化
-
構造器中初始化
物件例項化的過程
-
載入類元資訊
-
為物件分配記憶體
-
處理併發問題
-
屬性的預設初始化(零值初始化)
-
設定物件頭資訊
-
屬性的顯示初始化、程式碼塊中初始化、構造器中初始化
8.2. 物件記憶體佈局
8.2.1. 物件頭(Header)
物件頭包含了兩部分,分別是執行時元資料(Mark Word)和型別指標。如果是陣列,還需要記錄陣列的長度
執行時元資料
-
雜湊值(HashCode)
-
GC分代年齡
-
鎖狀態標誌
-
執行緒持有的鎖
-
偏向執行緒ID
-
翩向時間戳
型別指標
指向類元資料InstanceKlass,確定該物件所屬的型別。
8.2.2. 例項資料(Instance Data)
它是物件真正儲存的有效資訊,包括程式程式碼中定義的各種型別的欄位(包括從父類繼承下來的和本身擁有的欄位)
-
相同寬度的欄位總是被分配在一起
-
父類中定義的變數會出現在子類之前
-
如果CompactFields引數為true(預設為true):子類的窄變數可能插入到父類變數的空隙
8.2.3. 對齊填充(Padding)
不是必須的,也沒有特別的含義,僅僅起到佔位符的作用
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer() {
acct = new Account();
}
}
public class CustomerTest{
public static void main(string[] args){
Customer cust=new Customer();
}
}
小結
8.3. 物件的訪問定位
JVM是如何通過棧幀中的物件引用訪問到其內部的物件例項呢?
8.3.1. 控制代碼訪問
reference中儲存穩定控制代碼地址,物件被移動(垃圾收集時移動物件很普遍)時只會改變控制代碼中例項資料指標即可,reference本身不需要被修改
8.3.2. 直接指標(HotSpot採用)
直接指標是區域性變量表中的引用,直接指向堆中的例項,在物件例項中有型別指標,指向的是方法區中的物件型別資料