JVM對象問題一篇搞定! 對象創建-內存分配-內存大小估算-內存佈局-初始化過程-對象鎖升級-對象訪問方式

 
 

1.Object o = new Object() ;

問:虛擬機對Object o = new Object() 做了哪些工作?
答:
  1. 虛擬機遇到new 指令時,首先會檢查這個指令參數是否能在常量池中找到相關的引用,並檢查這個引用是否被加載、解析、初始化過;
  2. 如果沒有則先執行類的加載過程。
  3. 加載成功後,虛擬機開始爲創建對象申請內存空間;
  4. 虛擬機會將分配後的內存空間初始化爲零值(不包括對象頭);
    1. 如果使用TLAB ,則這一步驟會提前至TLAB 分配時進行。
    2. 初始化爲零值這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型對應的零值。
  5. 調用對象的<cinit> <init>構造方法;

2.分配內存

分配內存有兩種方式

  1. 指針碰撞bump to pointer:對於空間絕對規整的堆內存空間,通常是指垃圾回收器採用標記-整理算法後的堆內存空間;因爲內存空間是絕對規整的,就是使用中的內存在一邊,空閒的內存在另一邊,中間有一個指針作爲分界點,在分配內存時只需要將指針向空閒區域移動一段距離(新對象大小相等的距離); (ParNew 垃圾回收器,Serial 垃圾回收器)
  2. 空閒列表 free-list: 對於剩餘空間不規整的堆內存空間,通常是指垃圾回收器採用標記-清除算法後的堆內存空間;因爲內存空間不規整,這時候就需要虛擬機維護一個列表來記錄哪些內存空間是空閒的,哪些內存空間是佔用的。在空閒內存中找到一塊足夠大的空間分給新對象並記錄。(CMS 垃圾回收器)

3.對象的內存佈局

3.1 Mark Word:

  1. 用於存儲對象自身的運行時數據,如哈希碼(hash code) 、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳、對象分代年齡;
    • Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲儘量多的信息,會根據自己的狀態複用自己的存儲空間;
    • Mark Word 在32 位操作系統HotSpot 虛擬機中,如果對象未被鎖定狀態下,則32bit 中25bit 用於存儲對象哈希碼,4bit 用於存儲對象分代年齡,2bit 用於存儲鎖標誌位,1bit 固定爲0 ;
    • MarkWord 在64 位操作系統HotSpot 虛擬機中,開啓指針壓縮,那麼頭部存放Class指針的空間大小還是4字節,而Mark word 區域是8字節,頭部最小是12字節;
    • biased_lock : 對象是否啓用偏向鎖標記,佔1 bit , biased_lock = 1 標識對象啓用偏向鎖, biased_lock = 0 標識對象沒有偏向鎖,lock 和 biased_lock 共同標識對象處於什麼鎖狀態;
    • age : 對象分代年齡,佔4bit 。 GC 中,如果對象再Survivor 區複製一次,年齡+1 。當年齡達到閾值時,將晉升到老年代。默認情況,並行GC 的年齡閾值爲15,併發GC的年齡閾值是6 。 因爲age 是4位,所以最大是15,                      -XX: MaxTenuringThreshold 最大設置值爲15 。
    • identity_hashcode:  對象hashcode ,佔31 bit 。採用延遲加載技術,調用方法System.identityHashCode() 計算,結果放入對象頭中。當對象加鎖後(偏向、輕量級、重量級),MarkWord 的字節沒有足夠空間保存hashcode , 因此會移動到管理Monitor 中;
    • thread :  持有偏向鎖的線程ID;
    • epoch :  偏向鎖的時間戳;
    • ptr_to_lock_record :輕量級鎖狀態下,指向棧中鎖記錄的指針;
    • ptr_to_heavyweight_monitor: 重量級鎖(例如: synchronized 同步鎖)狀態下,指向對象監視器Monitor的指針; 

問: JVM 對象鎖升級過程?


答:無鎖態 - > 偏向鎖- > 輕量級鎖 - > 重量級鎖;

偏向鎖的獲取過程:

  1. 訪問Mark Word中偏向鎖的標識是否設置爲1,鎖標誌位是否爲01-則確認爲可偏向狀態
  2. 在爲可偏向狀態下,則判斷線程id是否指向當前線程,如果是,執行(5),否則執行(3)
  3. 如果線程id併爲指向當前線程,通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中的線程id設置爲當前線程id,然後執行(5);如果競爭失敗,執行(4)
  4. 如果CAS獲取偏向鎖失敗,則表示有競爭。當達到全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖(因爲偏向鎖是假設沒有競爭,但是這裏出現了競爭,要對偏向鎖進行升級),然後被阻塞在安全點的線程繼續往下執行同步代碼
  5. 執行同步代碼
偏向鎖的釋放點在於上述的第(4)步,只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的釋放過程爲:
  1. 需要等待全局安全點(在這個時間點上沒有字節碼正在執行)
  2. 它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態
  3. 偏向鎖釋放後恢復到未鎖定(標識位爲01)或輕量級鎖(標識位爲00)狀態

輕量級鎖的加鎖過程:

  1. 在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態,JVM首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲Displaced Mark Word,
  2. 拷貝對象頭中的Mark Word複製到鎖記錄中
  3. 拷貝成功後,JVM將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock Record裏的owner指針指向Object Mark Word,如果更新成功,則執行步驟(4),否則執行步驟(5)
  4. 如果更新動作成功,那麼當前線程就擁有了該對象的鎖,並且對象Mark Word的鎖標識位設置爲00,即表示此對象處於輕量級鎖狀態
  5. 如果更新動作失敗,JVM首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標識的狀態值變爲10,Mark Word中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。而當前線程變嘗試使用自旋來獲取鎖,自旋就是爲了不讓線程阻塞,而採用循環去獲取鎖的過程

重量級鎖

(更多線程競爭,導致更多的切換和等待,輕量級鎖膨脹爲重量級鎖-synchronized同步鎖,會指向一個監視器對象monitor,監視器對象來登記和管理排隊的過程)

  1. 進入監視器: entermonitor
  2. 離開監視器: leavemonitor

3.2 類型指針:

對象指向它的類元數據的指針,虛擬機通過這個指針確定這個對象是哪個類的實例。

如果對象是數組,那對象頭中有記錄數組長度的數據 ArrayLength ,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象大小,但是從數組的元數據中無法確定數組的大小;

3.3 實例數據(Instance Data)

  1. 是對象存儲的有效信息,代碼中所定義的各種類型的字段內容
  2. 這部分的存儲順序會收到虛擬機分配策略參數(FieldsAllocationStyle)和字段再Java源碼中定義順序的影響;

3.4 對齊填充(Padding)  - 8字節對齊規則

  1. 不是必然的存在,只是起到佔位符的作用;
  2. HotSpot VM的自動內存管理系統要求對象其實地址必須是8 byte 的整數倍,對象頭是8byte 的倍數,所以當對象實例數據部分沒有對齊時,就需要通過對齊填充不全;

問:如何估算對象大小?

 在32位操作系統上,new Object() ,JVM會分配8(MarkWord + 類型指針)字節的空間;

 如果new Integer() , 對象還有一個int 值,佔用4字節,對象佔用8+4 = 12 個字節。 對齊後 16字節。

Class A {
    int i;
    byte b;
    String str;
}
  1. 對象頭部佔用MarkWord 4 + 類型指針 4 = 8 字節;int 4字節;  byte 1字節;String 只是引用,4字節;
  2. 對象A 一共佔用 8 + 4 + 1+ 4 = 17 字節;對齊後是24 字節
  3. 分析對齊位置 ; 在HotSpot VM 中,對象 排布時,間隙是4字節基礎上的(在32位和64位壓縮模式下) ,上述例子,int 後面的byte ,空隙只剩下3字節,接下來String  對象引用需要4字節存放,因此byte 和對象引用之間就會有3字節對接,對象引用排布後,最後會有4字節對齊,因此結果上依然是7字節對齊;

注意:創建對象過程是非線程安全的,JVM使用兩種方式解決
  1. 採用"CAS+ 失敗重試方式"方式保證創建操作的原子性
  2. 採用TLAB ,對於每一個線程在Eden 中分配一小片區域,默認佔用1% 空間。
  3. 通過配置 -XX: +/- UseTLAB

問:對象一定在堆中分配嗎?

答:不一定! 

    如果開啓棧上分配則JVM 會優先進行棧上分配,此時JIT編譯器會採用逃逸分析(Escape Analysis)技術來分析出對象的引用的使用範圍,來決定是否要在堆上分配內存即將對象的內存分配從堆轉化爲棧;
    逃逸分析是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數數據流分析算法。
    逃逸分析的基本行爲就是分析對象動態作用域: 當一個對象在方法中被定義後,可能被外部方法所引用。
    從jdk1.7開始默認開啓逃逸分析: 關閉參數爲: -XX: -DoEscapeAnalysis
如果在棧上分配失敗(空間不足等原因)則嘗試TLAB (Thread Local Allocation Buffer) 線程本地分配緩衝區,默認佔用Eden空間大小的1%,可以使用配置參數 +XX: TLABWasteTargetPercent 進行配置大小。
JVM 會維護 refill_waste值,當對象大於當前剩餘TLAB 空間 且大於refill_waste 值時,則會選擇在堆中分配,若對象大於當前剩餘TLAB 空間 且小於refill_waste 值,則會廢棄當前TLAB,新建一個TLAB 存放對象。
 
堆是由所有線程共享的,是競爭資源,對於競爭資源,必須採取必要的同步,所以當使用new關鍵字在堆上分配對象時,是需要鎖的。存在鎖帶來的開銷,而且由於是對整個堆加鎖,相對而言鎖的粒度還是比較大的,影響效率。
但是TLAB和棧都是線程私有的,即避免了競爭。所以對於某些特殊情況,可以採取避免在堆上分配對象的辦法,以提高對象創建和銷燬的效率。
 

3. 初始化 <cinit> <init>

class X {
   static Log log = LogFactory.getLog(); // <clinit>
   private int x = 1;   // <init>
   X(){
      // <init>
   }
   static {
      // <clinit>
   }
}

 

  1. <init> 是instance實例構造器方法,對費靜態變量解析初始化;
  2. <cinit> 是class類構造器方法,是jvm 進行 加載-驗證-解析-初始化中初始化階段jvm 調用cinit 方法,對靜態變量、靜態代碼塊進行初始化;
    1.  編譯器收集的順序是由代碼語句在源文件中的順序決定的,靜態語句塊中只能訪問在靜態語句塊之前的變量。之後的變量可以賦值,但是不能訪問,代碼如下:
public class Test {
    static {
        i = 0;//變量賦值編譯通過
        System.out.print(i);//提示illegal forward reference  -"非法向前引用"
    }
    static int i = 1;
}

3.1 單個類 - 初始化過程

  1. 靜態變量 父類
  2. 靜態塊
  3. 普通變量-初始化塊
  4. 構造器

3.2 父子類 - 初始化過程

  1. 父類-靜態變量
  2. 父類-靜態塊
  3. 子類-靜態變量
  4. 子類-靜態塊
  5. 父類-普通變量
  6. 父類-實例初始化塊
  7. 父類-構造器
  8. 子類-普通變量
  9. 子類-實例初始化塊
  10. 子類-構造器
 
注:接口 
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<cinit> 方法,但接口與類不同的是,執行接口<cinit> 方法不需要先執行父接口的<cinit> 方法,只有當父接口中定義的變量使用時,父接口才會初始化。
接口的實現類再初始化時也一樣不會執行接口的<cinit>方法

static final 修飾的變量在”準備” 階段就開始初始化。

4.對象訪問;

  1. Object o = new Object() 中,上述描述的是new Object() 在Java堆中的形成過程,
  2. 而Object o 這部分將會反映在Java棧的本地變量中,作爲一個reference類型數據出現。reference 類型是指向對象的引用地址

4.1 句柄式訪問:

堆中有一片區域叫“句柄池”,是用於存放所有對象的地址和對象所屬類信息; 引用類型變量存放的地址是該對象再句柄池中的地址,訪問對象時首先通過該對象的句柄,然後根據句柄再去訪問該對象;圖:
 

4.2 直接指針訪問:

通過引用直接訪問該對象的地址,但對象所在內存空間需要額外的內存策略來記錄該對象在方法區中的類信息的地址。對於HotSpot,採用是直接指針訪問的方式;圖:

4.3 優缺點:

句柄式訪問好處是實際引用對象改變時只需要改句柄的指針;
而使用直接指針訪問速度較快,因爲節省了一次指針定位的時間開銷,但需要額外的策略存儲被引用對象的類信息。
 
 

~~如果對你有一絲幫助請加個關注。感謝 :)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章