10-对象实例化、内存布局与访问定位
對象實例化內(nèi)存布局與訪問定位
對象實例化
面試題
- 對象在JVM中是怎么存儲的?
- 對象頭信息里面有哪些東西?
- Java對象頭有什么?
從對象創(chuàng)建的方式 和 步驟開始說
對象創(chuàng)建方式
- new:最常見的方式、單例類中調(diào)用getInstance的靜態(tài)類方法,XXXFactory的靜態(tài)方法
- Class的newInstance方法:在JDK9里面被標記為過時的方法,因為只能調(diào)用空參構造器
- Constructor的newInstance(XXX):反射的方式,可以調(diào)用空參的,或者帶參的構造器
- 使用clone():不調(diào)用任何的構造器,要求當前的類需要實現(xiàn)Cloneable接口中的clone接口
- 使用序列化:序列化一般用于Socket的網(wǎng)絡傳輸
- 第三方庫 Objenesis
創(chuàng)建對象的步驟
判斷對象對應的類是否加載、鏈接、初始化
虛擬機遇到一條new指令,首先去檢查這個指令的參數(shù)能否在Metaspace的常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載,解析和初始化。(即判斷類元信息是否存在)。如果沒有,那么在雙親委派模式下,使用當前類加載器以ClassLoader + 包名 + 類名為key進行查找對應的 .class文件,如果沒有找到文件,則拋出ClassNotFoundException異常,如果找到,則進行類加載,并生成對應的Class對象。
為對象分配內(nèi)存
首先計算對象占用空間的大小,接著在堆中劃分一塊內(nèi)存給新對象。如果實例成員變量是引用變量,僅分配引用變量空間即可,即4個字節(jié)大小
-  如果內(nèi)存規(guī)整:指針碰撞 
-  如果內(nèi)存不規(guī)整 - 虛擬表需要維護一個列表
- 空閑列表分配
 
如果內(nèi)存是規(guī)整的,那么虛擬機將采用的是指針碰撞法(Bump The Point)來為對象分配內(nèi)存。
意思是所有用過的內(nèi)存在一邊,空閑的內(nèi)存放另外一邊,中間放著一個指針作為分界點的指示器,分配內(nèi)存就僅僅是把指針指向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是Serial ,ParNew這種基于壓縮算法的,虛擬機采用這種分配方式。一般使用帶Compact(整理)過程的收集器時,使用指針碰撞。
如果內(nèi)存不是規(guī)整的,已使用的內(nèi)存和未使用的內(nèi)存相互交錯,那么虛擬機將采用的是空閑列表來為對象分配內(nèi)存。意思是虛擬機維護了一個列表,記錄上那些內(nèi)存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的內(nèi)容。這種分配方式稱為 “空閑列表(Free List)”
選擇哪種分配方式由Java堆是否規(guī)整所決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
處理并發(fā)問題
- 采用CAS失敗重試(compare and swap)、區(qū)域加鎖保證更新的原子性
- 每個線程預先分配TLAB - 通過設置 -XX:+/-UseTLAB參數(shù)來設置 - 在Eden區(qū)給每個線程分配一塊區(qū)域(JDK8已默認有該功能)
 
初始化分配到的空間(即:對象、屬性默認初始化)
- 所有屬性設置默認值,保證對象實例字段在不賦值可以直接使用(下面第一種賦值方式)
給對象屬性賦值的操作
- 屬性的默認初始化
- 顯式初始化
- 代碼塊中的初始化
- 構造器初始化
設置對象的對象頭
將對象的所屬類(即類的元數(shù)據(jù)信息)、對象的HashCode和對象的GC信息、鎖信息等數(shù)據(jù)存儲在對象的對象頭中。這個過程的具體設置方式取決于JVM實現(xiàn)。
執(zhí)行init方法進行初始化(即:屬性的顯示初始化、代碼塊中初始化、構造器中初始化)
在Java程序的視角看來,初始化才正式開始。初始化成員變量,執(zhí)行實例化代碼塊,調(diào)用類的構造方法,并把堆內(nèi)對象的首地址賦值給引用變量。(下面2、3、4中賦值方式)
給對象屬性賦值的操作
- 屬性的默認初始化
- 顯式初始化
- 代碼塊中的初始化
- 構造器初始化
因此一般來說(由字節(jié)碼中跟隨invokespecial指令所決定),new指令之后會接著就是執(zhí)行方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完成創(chuàng)建出來。
init方法:類的構造器對應的初始化的方法
 cinit方法:類的靜態(tài)屬性的初始化的方法
對象實例化的過程
- 加載類元信息
- 為對象分配內(nèi)存
- 處理并發(fā)問題
- 屬性的默認初始化(零值初始化)
- 設置對象頭信息
- 屬性的顯示初始化、代碼塊中初始化、構造器中初始化
對象內(nèi)存布局
對象頭
對象頭包含了兩部分,分別是 運行時元數(shù)據(jù)(Mark Word)和 類型指針
如果是數(shù)組,還需要記錄數(shù)組的長度
運行時元數(shù)據(jù)
- 哈希值(HashCode)
- GC分代年齡
- 鎖狀態(tài)標志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
類型指針
指向類元數(shù)據(jù)InstanceClass,確定該對象所屬的類型。指向的其實是方法區(qū)中存放的類元信息
實例數(shù)據(jù)(Instance Data)
說明
不是必須的,也沒有特別的含義,僅僅起到占位符的作用
小結
public class Customer{//顯式初始化int id = 1001;String name;Account acct;//代碼塊中的初始化{name = "匿名客戶";}//構造器初始化public Customer(){acct = new Account();} } class Account{} public class CustomerTest{public static void main(String[] args){Customer cust = new Customer();} }動態(tài)鏈接是將方法區(qū)中該棧幀所屬方法的符號引用轉(zhuǎn)為直接引用
對象的訪問定位
圖示
JVM是如何通過棧幀中的對象引用(局部變量)訪問到其內(nèi)部的對象實例呢?
 
對象訪問的兩種方式
句柄訪問
句柄訪問就是說棧的局部變量表中,記錄的對象的引用,然后在堆空間中開辟了一塊空間,也就是句柄池
優(yōu)點
reference中存儲穩(wěn)定句柄地址,對象被移動(垃圾收集時移動對象很普遍)時只會改變句柄中實例數(shù)據(jù)指針即可,reference本身不需要被修改
直接指針(HotSpot采用)
直接指針是局部變量表中的引用,直接指向堆中的實例,在對象實例中有類型指針,指向的是方法區(qū)中的對象類型數(shù)據(jù)
總結
以上是生活随笔為你收集整理的10-对象实例化、内存布局与访问定位的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 07-本地方法栈
- 下一篇: 15-垃圾回收相关算法
