物件例項化記憶體佈局與訪問定位
從各自具體的記憶體分配上來講
new 的物件放在堆中
物件所屬的型別資訊是放在方法區的
方法當中的區域性變數放在棧空間
這 new 的物件怎麼把三塊粘合到一起 就是這章的內容了
物件例項化
面試題
美團:
- 物件在 JVM 中是怎麼儲存的?
- 物件頭資訊裡面有哪些東西?
螞蟻金服:二面
- Java 物件頭有什麼?
從物件建立的方式和步驟開始說
物件建立方式
- new:最常見的方式,單例類中呼叫 getInstance 的靜態類方法,XXXFactory 的靜態方法
- Class 的 newInstance 方法:反射的方式,在 JDK9 裡面被標記為過時的方法,因為只能呼叫空參構造器,許可權必須是 public
- Constructor 的 newInstance(XXX):反射的方式,可以呼叫空參、帶參的構造器,許可權沒有要求
- 使用
clone()
:不呼叫任何的構造器,要求當前類需要實現 Cloneable 介面中的clone()
方法 - 使用反序列化:反序列化一般用於 Socket 的網路傳輸,從檔案中、從網路中獲取一個物件的二進位制流
- 第三方庫 Objenesis
建立物件的步驟
判斷物件對應的類是否載入、連結、初始化
虛擬機器遇到一條 new 指令,首先去檢查這個指令的引數能否在 Metaspace 的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入、解析和初始化。(即判斷類元資訊是否存在)。如果沒有,那麼在雙親委派模式下,使用當前類載入器以“ClassLoader + 包名 + 類名”為 key 進行查詢對應的 .class 檔案,如果沒有找到檔案,則丟擲 ClassNotFoundException 異常,如果找到,則進行類載入,並生成對應的 Class 類物件。
為物件分配記憶體
首先計算物件佔用空間的大小,接著在堆中劃分一塊記憶體給新物件。
如果例項成員變數是引用變數,僅分配引用變數空間即可,即 4 個位元組大小
如果記憶體規整:指標碰撞
如果記憶體是規整的,那麼虛擬機器將採用的是指標碰撞法(Bump The Point)來為物件分配記憶體。
意思是所有用過的記憶體在一邊,空閒的記憶體放另外一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標指向空閒那邊挪動一段與物件大小相等的距離罷了。如果垃圾收集器選擇的是 Serial,ParNew 這種基於壓縮演算法的,虛擬機器採用這種分配方式。一般使用帶有 Compact(整理)過程的收集器時,使用指標碰撞。
如果記憶體不規整
- 虛擬表需要維護一個列表
- 空閒列表分配
如果記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,那麼虛擬機器將採用的是空閒列表來為物件分配記憶體。意思是虛擬機器維護了一個列表,記錄上那些記憶體塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的內容。這種分配方式成為了 “空閒列表(Free List)”
選擇哪種分配方式由 Java 堆是否規整所決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
處理併發問題
- 採用 CAS 配上失敗重試保證更新的原子性
- 每個執行緒預先分配 TLAB - 通過設定
-XX:+UseTLAB
引數來設定(區域加鎖機制)- 在 Eden 區給每個執行緒分配一塊區域
初始化分配到的記憶體空間
給物件屬性賦值的操作
- 屬性的預設初始化
- 顯示初始化/程式碼塊中的初始化(這倆並列執行,具體先後得看順序)
- 構造器初始化
- 所有屬性設定預設值,保證物件例項欄位在不賦值可以直接使用
設定物件的物件頭
將物件的所屬類(即類的後設資料資訊)、物件的 HashCode 和物件的 GC 資訊、鎖資訊等資料儲存在物件的物件頭中。這個過程的具體設定方式取決於 JVM 實現。
執行init方法進行初始化
在 Java 程式的視角看來,初始化才正式開始。初始化成員變數,執行例項化程式碼塊,呼叫類的構造方法,並把堆內物件的首地址賦值給引用變數
因此一般來說(由位元組碼中跟隨 invokespecial 指令所決定),new 指令之後會接著就是執行方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全建立出來。
物件例項化的過程
- 載入類元資訊
- 為物件分配記憶體
- 處理併發問題
- 屬性的預設初始化(零值初始化)
- 設定物件頭資訊
- 屬性的顯示初始化、程式碼塊中初始化、構造器中初始化
物件記憶體佈局
物件頭
物件頭包含了兩部分,分別是
- 執行時後設資料(Mark Word)
- 型別指標
如果是陣列,還需要記錄陣列的長度
執行時後設資料
- 雜湊值(HashCode):物件的首地址值,換成雜湊以免查詢耗時
- GC 分代年齡:年齡計數器 age,達到 16 就進入老年代了
- 鎖狀態標誌
- 執行緒持有的鎖
- 偏向執行緒 ID
- 偏向時間戳
型別指標
指向類後設資料 InstanceKlass,確定該物件所屬的型別。指向的其實是方法區中存放的類元資訊
並不是所有的物件都會保留型別指標。
例項資料(Instance Data)
說明
它是物件真正儲存的有效資訊,包括程式程式碼中定義的各種型別的欄位(包括從父類繼承下來的和本身擁有的欄位)
規則
- 父類中定義的變數會出現在子類之前
- 相同寬度的欄位總是被分配在一起
- 如果 CompactFields 引數為 true(預設為 true):字類的窄變數可能插入到父類變數的空隙
對齊填充(Padding)
不是必須的,也沒有特別的含義,僅僅起到佔位符的作用
小結
物件的訪問定位
圖示
JVM 是如何通過棧幀中的物件引用訪問到其內部的物件例項呢?
物件訪問的兩種方式
控制程式碼訪問
控制程式碼訪問就是說棧的區域性變數表中,記錄的物件的引用,然後在堆空間中開闢了一塊空間,也就是控制程式碼池
優點:
reference 中儲存穩定控制程式碼地址,物件被移動(垃圾收集時移動物件很普遍)時只會改變控制程式碼中例項資料指標即可,reference 本身不需要被修改。
直接指標(HotSpot 採用)
直接指標是區域性變數表中的引用,直接指向堆中的例項,在物件例項中有型別指標,指向的是方法區中的物件型別資料
此時如果堆中物件的位置改變,那麼 reference 也要跟著改變。如 “垃圾回收相關演算法”中的複製演算法。