天天看點

【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位

1、對象建立過程

單純從語言層面,建立一個對象,可以通過new、反射、複制、反序列化等等。接下來,我們探究以下在虛拟機中,對象的建立是一個什麼樣的過程。

我們以虛拟機遇到一個new指令開始:

  • 首先檢查這個指令的參數是否能在常量池中定位到一個類的符号引用
  • 檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,就先執行相應的類加載過程
  • 類加載檢查通過後,接下來虛拟機将為新生對象配置設定記憶體。記憶體配置設定有兩種方式,指針碰撞(Bump The Pointer)、空閑清單(Free List)
    • 指針碰撞:假設Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閑的記憶體被放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離,這種配置設定方式稱為“指針碰撞”
    • 如果Java堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體互相交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄,這種配置設定方式稱為“空閑清單”
    • 兩種方式的選擇由Java堆是否規整決定
    • Java堆規整由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定
  • 記憶體配置設定完成之後,虛拟機将配置設定到的記憶體空間(但不包括對象頭)都初始化為零值。
  • 接下來設定對象頭,請求頭裡包含了對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊。

這個過程大概圖示如下:

【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位

配置設定記憶體線程安全問題:對象建立在虛拟機中是非常頻繁的行為,即使僅僅修改一個指針所指向的位置,在并發情況下也并不是線程安全的,可能出現正在給對象A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。

線程安全問題有兩種解可選方案:

  • 一種是對配置設定記憶體空間的動作進行同步處理——實際上虛拟機是采用CAS配上失敗重試的方式保證更新操作的原子性
  • 另外一種是把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定緩沖(Thread Local Allocation Buffer,TLAB),哪個線程要配置設定記憶體,就在哪個線程的本地緩沖區中配置設定,隻有本地緩沖區用完了,配置設定新的緩存區時才需要同步鎖定。

從虛拟機角度來看,設定完對象頭資訊以後初始化就已經完成了,但是對于Java程式而言,new指令之後會接着執行<init> ()方法,對對象進行初始化,這樣一個真正可用的對象才算完全被構造出來。

【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位

2、對象的記憶體布局

在HotSpot虛拟機裡,對象在堆記憶體中的存儲布局可以劃分為三個部分:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。

【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位

HotSpot虛拟機對象的對象頭部分包括兩類資訊。第一類是用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32個比特和64個比特,官方稱它為“Mark Word”。

考慮到虛拟機的空間效率,Mark Word被設計成一個有着動态定義的資料結構,以便在極小的空間記憶體儲盡量多的資料,根據對象的狀态複用自己的存儲空間。

例如在64位的HotSpot虛拟機中,如對象未被同步鎖鎖定的狀态下,Mark Word的64個比特存儲空間中的31個比特用于存儲對象哈希碼,4個比特用于存儲對象分代年齡,2個比特用于存儲鎖标志位,在其他狀态(輕量級鎖、重量級鎖、偏向鎖)下對象的存儲内容變化如圖示。

對象頭的另外一部分是類型指針,即對象指向它的類型中繼資料的指針,Java虛拟機通過這個指針來确定該對象是哪個類的執行個體。并不是所有的虛拟機實作都必須在對象資料上保留類型指針,查找對象的中繼資料資訊并不一定要經過對象本身,

如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象的大小,但是如果數組的長度是不确定的,将無法通過中繼資料中的資訊推斷出數組的大小。

3、對象的通路定位

Java程式會通過棧上的reference資料來操作堆上的具體對象。由于reference類型在《Java虛拟機規範》裡面隻規定了它是一個指向對象的引用,并沒有定義這個引用應該通過什麼方式去定位、通路到堆中對象的具體位置,是以對象通路方式也是由虛拟機實作而定的,主流的通路方式主要有使用句柄和直接指針兩種:

  • 如果使用句柄通路的話,Java堆中将可能會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自具體的位址資訊,其結構如圖所示:
【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位
  • 如果使用直接指針通路的話,Java堆中對象的記憶體布局就必須考慮如何放置通路類型資料的相關資訊,reference中存儲的直接就是對象位址,如果隻是通路對象本身的話,就不需要多一次間接通路的開銷,如圖所示:
【JVM進階之路】三:探究虛拟機對象1、對象建立過程2、對象的記憶體布局3、對象的通路定位

這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要被修改。

使用直接指針來通路最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象通路在Java中非常頻繁,是以這類開銷積少成多也是一項極為可觀的執行成本。

HotSpot虛拟機主要使用直接指針來進行對象通路。