1. 程式人生 > >學習筆記:深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(2)

學習筆記:深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(2)

保留 頻繁 深入 一是 init方法 對象的引用 整理 緩沖 出現

學習筆記:深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(2)

三、HotSpot虛擬機對象探秘

1.對象的創建

? 在Java程序運行過程中時刻都有對象被創建。在語言層面上,創建對象(例如克隆、反序列化)通常僅僅是一個關鍵字new而已,而在虛擬機中,對象(普通的Java對象,不包括數組和Class字節碼文件對象)的創建又是怎樣一個過程呢?

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

? 在類加載檢查通過後,接下來虛擬機將為新生對象分配內存

。對象所需內存的大小在類加載完成後便可完全確定,為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。假設Java堆內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放一個指針作為分界點的指示器,那所分配內存就是把指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式成為“指針碰撞”(Bump the Point)。如果Java堆中的內存並不規整,已使用的內存和空閑的內存相互交錯,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,常采用空閑列表。

? 除了內存劃分,另一個需要考慮的問題是線程安全問題。創建對象在虛擬機是非常頻繁的行為,即使僅是修改指針指向的位置,在並發情況下也並不是線程安全的(可能正在給A分配內存,指針還沒來得及改,對象B就又使用了原來的指針來分配內存)。解決方案有兩種:一是對分配內存的行為做同步處理(實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性)。二是把內存分配的動作按照線程劃分在不同的空間之中進行(每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB中分配,TLAB用完才進行同步鎖定)。

? 內存分配完成後,虛擬機要將分配到的內存空間初始化為零值(不包括對象頭)。如果使用TLAB,這一過程也能提前至TLAB分配時進行。

? 接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的HashCode、對象的GC分代年齡等。這些信息存放在對象的對象頭(Object Header)中。根據虛擬機當前運行狀態不同,對象頭會有不同的設置方式。

? 上述工作完成後,在虛擬機視角看,一個新的對象已經產生了。但從Java程序的視角看,對象創建才剛開始,init初始化方法還未執行,所有字段都為零。執行new指令後會接著執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來。

對象的創建過程: 類加載檢查-->分配內存(線程安全問題)-->初始化內存空間-->設置對象頭-->init初始化方法

2.對象的內存布局

? 在HotSpot虛擬機中,對象的內存布局分為3塊:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

2.1對象頭

? 對象頭包括兩部分信息,第一部分存儲對象自身的運行時數據,如HashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。

? 對象頭的另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過指針來確定這個對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針,也即查找對象的元數據信息並不一定要經過對象本身。如果對象是一個數組,那麽在對象頭中還要有一塊用於記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是數組的元數據中無法確定數組大小。

2.2實例數據

? 實例數據部分是對象真正存儲的有效信息,也是在程序代碼中定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。這部分的存儲順序會受到虛擬機分配策略參數(Field Allocation Style)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略為longs/doubles,ints,shorts/chars,bytes/booleans,opps(Ordinary Object Pointers),從分配策略可以看出,相同寬度的字段總是被分配在一起。在這個前提下,父類中定義的變量會出現在子類之前,如果CompactFields參數為true(默認為true),那麽子類中較窄的變量也可能會插入到父類變量的空隙之中。

2.3對齊填充

? 對齊填充並不是必然存在的,也沒有特殊含義,它僅僅起著占位符的作用。HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,也就是對象的大小必須是8的整數倍,而對象頭部分正好是8字節的倍數(1或2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

3.對象的訪問

? 建立對象是為了使用,Java程序需要通過棧上的reference數據來操作堆中的具體對象。由於reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中對象的具體位置,所以對象訪問的方式也取決於虛擬機的實現。目前主流的訪問方式由使用句柄和直接指針兩種。

  • 如果使用句柄訪問,那麽堆中會劃分一塊內存作為句柄池reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

技術分享圖片

  • 如果使用直接指針訪問,那麽堆中對象的布局就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。

技術分享圖片

? 兩種對象訪問方式各有優勢,使用句柄訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改。

? 使用直接指針訪問方式最大好處是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項可觀的執行成本。

HotSpot虛擬機中是使用直接指針進行對象訪問的。

學習筆記:深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(2)