對象的實例化
創建對象的方式
- new
- 最常見的方式
- 變形:單例類中調用getInstance的靜態類方法
- 變形:XXXFactory的靜態方法
- Class的newInstance方法
在JDK9裏面被標記爲過時的方法,因爲只能調用空參構造器,並且權限必須爲 public - Constructor的newInstance(Xxxx)
反射的方式,可以調用空參的,或者帶參的構造器 - 使用clone()
不調用任何的構造器,要求當前的類需要實現Cloneable接口中的clone方法 - 使用序列化
序列化一般用於Socket的網絡傳輸 - 第三方庫 Objenesis
對象創建過程
首先回顧給對象的屬性賦值的操作
- 屬性的默認初始化
- 顯示初始化
- 代碼塊中初始化
- 構造器初始化
從字節碼看待對象的創建過程
對應的字節碼
- 0 new #2 <java/lang/Object>
#2 在字符串常量池中找到對應的 是class java/lang/Object- 首先判斷方法區是不是已經對這個類已經加載了,沒有加載的話需要去java.lang.Object把這個類加載到方法區
- 然後在堆空間中把創建這個對象需要的空間開闢好
- 對變量進行零值初始化
- dup
操作數棧中有當前生成的變量的引用,然後dup把這個引用複製一份。相當於就有兩個指向堆空間這個對象的引用的實體,一個用於賦值操作,一個用於調用相關方法 - invokespecial #1 <java/lang/Object.>
通過#1在常量池中找到最終要調用的是<init>方法
給對象屬性賦值的2.顯示初始化3. 代碼塊中初始化4. 構造器初始化 就是在這部分完成的
創建對象的步驟(具體的執行步驟)
-
判斷對象對應的類是否加載、連接、初始化
(也就是看創建對象所屬的類在內存中是不是已經存在)- 虛擬機遇到一條new指令,首先去檢查這個指令的參數能否在Metaspace的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載,解析和初始化。(即判斷類元信息是否存在)。
- 如果該類沒有加載,那麼在雙親委派模式下,使用當前類加載器以ClassLoader + 包名 + 類名爲key進行查找對應的.class文件
- 如果沒有找到文件,則拋出ClassNotFoundException異常
- 如果找到,則進行類加載,並生成對應的Class對象。
-
爲對象分配內存
- 首先計算對象佔用空間的大小,接着在堆中劃分一塊內存給新對象。
如果實例成員變量是引用變量,僅分配引用變量空間即可,即4個字節大小。
選擇哪種分配方式由Java堆是否規整所決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定 - 如果內存規整:採用指針碰撞分配內存
指針碰撞: 意思是所有用過的內存在一邊,空閒的內存放另外一邊,中間放着一個指針作爲分界點的指示器,分配內存就僅僅是把指針往空閒內存那邊挪動一段與對象大小相等的距離罷了。
一般使用帶Compact(整理)過程的收集器時,使用指針碰撞。如Serial ,ParNew這兩種基於壓縮算法的垃圾回收器,因爲內存比較規整。 - 如果內存不規整:虛擬機需要維護空閒列表
“空閒列表(Free List)”分配方式: 虛擬機維護了一個列表,記錄上哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。
標記清除算法CMS清理過後的堆內存,就會存在很多內存碎片。就會使用這種方式
- 首先計算對象佔用空間的大小,接着在堆中劃分一塊內存給新對象。
-
處理併發安全問題
- 採用CAS失敗重試、區域加鎖保證更新的原子性
- 每個線程預先分配TLAB
-
初始化分配到的空間 ,即對對象的屬性進行默認初始化
所有屬性設置默認值,保證對象實例字段在不賦值可以直接使用
-
設置對象的對象頭
將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC信息、鎖信息等數據存儲在對象的對象頭中。這個過程的具體設置方式取決於JVM實現。 -
執行init方法進行初始化
給對象屬性賦值的2.顯示初始化3. 代碼塊中初始化4. 構造器初始化 就是在這部分完成的
在Java程序的視角看來,初始化才正式開始。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量因此一般來說(由字節碼中跟隨invokespecial指令所決定),new指令之後會接着就是執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完成創建出來。
對象的內存佈局
對象在堆空間的內存佈局有三部分:
- 對象頭
對象頭包含兩部分:運行時元數據(Mark Word)和類型指針- 運行時元數據
- 哈希值(HashCode),可以看作是堆中對象的地址
- GC分代年齡(年齡計數器)
- 鎖狀態標誌
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
- 類型指針
指向類元數據InstanceKlass,確定該對象所屬的類型(當前這個對象是哪個類的)。指向的其實是方法區中存放的類元信息 - 如果對象是數組,還需要記錄數組的長度
- 運行時元數據
- 實例數據
對象真正存儲的有效信息,包括程序代碼中定義的各種類型的字段(包括從父類繼承下來的和本身擁有的字段)。
規則:
父類中定義的變量會出現在子類之前(父類在子類之前加載)
相同寬度的字段總是被分配在一起
如果CompactFields參數爲true(默認爲true):子類的窄變量可能插入到父類變量的空隙 - 對齊填充
不是必須的,也沒特別含義,僅僅起到佔位符的作用
圖解內存佈局
Customer類
當我們new 一個Customer() 內存佈局如下
對象的訪問定位
JVM是如何通過棧幀中的對象引用訪問到其內部的對象實例呢?
定位,通過棧上reference訪問
棧幀的局部變量中的對象的引用記錄了堆空間中這個對象的地址值
對象的兩種訪問方式
對象的訪問方式有兩種:
- 句柄訪問
- 直接訪問(HotSpot 虛擬機採用這一種)
句柄訪問
棧的局部變量表記錄了對象引用reference,堆空間的開闢了句柄池,句柄池中有很多句柄,一個對象對應一個句柄,句柄記錄到對象實例數據的指針和到對象類型數據的指針。
缺點:在堆空間中開闢了一塊空間作爲句柄池,句柄池本身也會佔用空間;通過兩次指針訪問才能訪問到堆中的對象,效率低
優點:reference中存儲穩定句柄地址,對象被移動(垃圾收集時移動對象很普遍)時只會改變句柄中實例數據指針即可,reference本身不需要被修改
直接指針
棧的局部變量表記錄了對象引用reference,引用直接指向堆空間對象實體,對象實體中的類型指針指向方法中的類元數據。
HotSpot採用這種方式。
優點:直接訪問對象實例效率高,且不用額外開闢空間。
缺點:對象被移動(垃圾收集時移動對象很普遍)時需要修改 reference 的值
面試題
對象在JVM中是怎麼存儲的?(美團)
對象頭信息裏面有哪些東西?(美團)
java對象裏頭有什麼(螞蟻金服二面)