HotSpot虛擬機對象探祕(六)

對象的創建
  • 虛擬機遇到一條new指令時,首先檢查這個指令的參數能否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有那麼首先執行相應的類的加載過程。

  • 在類加載檢查通過之後,接下來虛擬機需要爲新生的對象分配內存。對象所需的內存大小在類加載完成之後便可完全確定,爲對象分配內存的任務等同於把一塊確定大小的內存從java堆中劃分出來。假設Java堆內存是完全規整的,所有用過的內存放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,這種分配方式成爲“指針碰撞”。如果java堆空間不是規整的,已使用內存和空閒內存都是相互交叉的,那就沒辦法簡單的使用指針碰撞了,虛擬機必須維護一個列表,記錄哪些內存是可用的,在分配內存的時候從列表中找到一塊足夠大的空間劃分給對象,並更新列表上的記錄,這種分配方式成爲“空閒列表”。選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此,使用Serial、ParNew 等帶Compact 過程的收集器採用指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器,通常採用空閒列表。

  • 除了可用空間的劃分之外,對象在虛擬機中創建是非常頻繁的行爲,即使是僅僅修改一個指針所在的位置,在併發場景下也並不是線程安全的,可能正在分配A對象所需的內存,指針還沒來得及修改指針,對象B也使用了原來的指針分配內存。解決這個問題有兩種方案:一種是,對象分配內存空間的動作進行同步處理——實際採用CAS配上失敗重試機制保證更新操作的原子性;另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一塊內存,成爲本地線程分配緩衝(Thread Local Allocate Buffer,TLAB)。哪個線程需要分配內存,就在哪個線程的TLAB上分配。,只有用完並重新分配TLAB時,才需要同步確定,虛擬機是否使用TLAB,可以通過參數-XX: +/- UseTLAB參數設定。

    TLAB:
    TLAB,避免了多線程衝突,在給對象分配內存時,每個線程使用自己的TLAB,這樣可以避免線程同步,提高了對象分配的效率。TLAB本身佔用eEden區空間,在開啓TLAB的情況下,虛擬機會爲每個Java線程分配一塊TLAB空間。參數-XX:+UseTLAB開啓TLAB,JVM默認是開啓的,TLAB空間的內存非常小,缺省情況下僅佔有整個Eden空間的1%,可以通過選項-XX:TLABWasteTargetPercent設置TLAB空間所佔用Eden空間的百分比大小。TLAB空間一般不會很大,因此大對象無法在TLAB上進行分配,總是會直接分配在堆上。TLAB空間由於比較小,因此很容易裝滿。比如,一個100K的空間,已經使用了80KB,當需要再分配一個30KB的對象時,肯定就無能爲力了。這時虛擬機會有兩種選擇,第一,廢棄當前TLAB,這樣就會浪費20KB空間;第二,將這30KB的對象直接分配在堆上,保留當前的TLAB,這樣可以希望將來有小於20KB的對象分配請求可以直接使用這塊空間。實際上虛擬機內部會維護一個叫作refill_waste的值,當請求對象大於refill_waste時,會選擇在堆中分配,若小於該值,則會廢棄當前TLAB,新建TLAB來分配對象。這個閾值可以使用TLABRefillWasteFraction來調整,它表示TLAB中允許產生這種浪費的比例。默認值爲64,即表示使用約爲1/64的TLAB空間作爲refill_waste。默認情況下,TLAB和refill_waste都會在運行時不斷調整的,使系統的運行狀態達到最優。如果想要禁用自動調整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,並使用-XX:TLABSize手工指定一個TLAB的大小。-XX:+PrintTLAB可以跟蹤TLAB的使用情況。一般不建議手工修改TLAB相關參數,推薦使用虛擬機默認行爲。

  • 接下來虛擬機需要對對象進行必要的參數設置,例如這個對象是哪個類的實例、如何才能找到類的元信息、對象的Hash碼、對象的GC分代年齡這些信息都會設置在對象的頭(Object Header)

  • 上面的工作完成之後,從虛擬機視角,一個新的對象就產生了,從java視角來看,對象創建纔剛剛開始——<init> 方法還沒有執行,所有的字段還爲0,所以執行new命令之後按照程序員的意願初始化之後,一個真正可用的對象纔算產生出來。

對象內存分配流程圖
  • 類加載以及內存分配流程
    類加載以及內存分配流程
  • 對象在堆內存的分配流程圖
    對象在堆內存的分配流程圖
對象的內存佈局

在HotSpot虛擬機中,對象在內存找那個存儲的佈局可以分爲3塊區域:對象頭(Hesder)實例數據(Instance)對其填充(Padding)

  • 對象頭:頭信息主要包含兩部分,第一部分用戶存儲對象自身的運行時數據如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位虛擬機(未開啓指針壓縮)中分別爲:32bit和64bit。官方稱之爲“Mark word”。對象需要存儲的運行時的數據很多,其實已經超過了32位和64位Bitmap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定長度的數據結構以便在極小的空間內存存儲儘量多的信息,它會根據對象的狀態複用自己的存儲空間。另外一部分是類型指針,即對象指向他的類元數據的指針,虛擬機通過這個指針確定對象是哪個類的實例,並不是所有的虛擬機實現都必須在對象數據上保留類型指針是通過,換句話說,查找對象的元數據信息並不一定經過對象本身。如果對象爲一個數組,那麼對象頭還必須有一塊用於記錄數組長度的數據。
    Mark Word在不同的鎖狀態下存儲的內容不同,在32位JVM中是這麼存的:
    對象頭信息
  • 實例數據:是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的還是子類定義的,都需要記錄下來。這部分的存儲順序會受虛擬機的分配策略參數(FiledsAllocationStype)和字段在Java源碼中定義的順序影響。HotSpot的分配策略爲longs/doubles、ints、shorts/char、bytes、booleans、oops(Ordinary Object Pointers),從分配策略可以看出,相同寬度的字段會被分配到一起,在滿足這個條件的前提下,在父類定義的變量也會出現在子類之前。如果CompactFields參數值爲true(默認爲true),那麼子類之中較窄的變量也可能插入到分類變量的空隙之中。
  • 對象填充:對象填充不是必然存在的,也沒有特別的含義,它僅僅是佔位符的左右。主要是因爲HotSpot JVM內存管理系統要求對象起始地址必須爲8的整數倍,也就是對象大小必須爲8的整數倍,對象頭部分正好是8的整數倍(1倍或者2倍),因此對象實例數據部分沒有對齊的,就需要通過填充方式來補全。
對象的訪問定位

建立對象主要是爲了使用對象,我們的Java程序主要是通過棧上的Reference數據來操作堆上的具體對象,Reference只規定了一個指向對象的引用,並沒有定義這個引用對象通過何種方式去定位和訪問堆上的對象的具體位置,所以對象訪問取決於虛擬機的自己的實現而定。目前主流的方式主要有句柄直接指針

  • 句柄訪問: 使用句柄訪問的話,Java堆中將劃分一塊內存出來作爲句柄池,Reference中存儲的就是對象的句柄池,而句柄池包含了對象實例數據與數據類型各自的具體地址信息如圖:
    句柄訪問
  • 直接指針:使用直接指針訪問的話,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而Reference中存儲的直接就是對象地址,如圖:
    直接指針

句柄訪問和直接指針訪問的優缺點:
使用句柄的最大好處就是Reference中存儲的是隱定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而Reference本身不需要修改。直接指針最大的好處是速度快,節省了一次指針的定位開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多也是一項非常可觀的執行成本。Sun HotSpot虛擬機主要是採用
直接指針的方式訪問的對象的。從整體軟件開發範圍來看,各種語言和框架使用句柄訪問的情況也十分常見。

在這裏插入圖片描述

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