天天看點

對象的建立、記憶體布局、對象的通路定位

前一篇文章我們介紹了Java虛拟機的運作時資料區域之後,我們大緻了解了虛拟機記憶體的概況。接下來,我們将一起學習對象是如何建立、如何布局

以及如何通路的。讨論這個問題需要限定在具體的虛拟機和集中在某一個記憶體區域上才有意義。我們這個所說的是Sun的HotSpot虛拟機的Java堆記憶體

區域,深入探讨HotSpot虛拟機在Java堆中對象的配置設定、布局和通路全過程。本文大綱:

一、 對象的建立

二、 對象的記憶體布局

三、 對象的通路定位

一、 對象的建立

在語言層面上,建立對象(例如克隆、反序列化)通常僅僅是一個new關鍵字而已,而在虛拟機中,對象(穩重探讨的對象限于普通對象,不包括數組

和Class對象等)的建立又是怎樣一個過程呢?

虛拟機遇到一個new指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并檢查這個符号引用代表的類是否已經被加載、

解析和初始化過。如果沒有,需要先執行相應的類加載過程(類加載将在以後的文章中介紹)。

在類加載檢查通過後,接下倆虛拟機将為新生對象配置設定内。對象所需記憶體的大小在類加載完成後便可完全确定,為對象配置設定空間的任務等同于把

一塊确定大小的記憶體從Java堆中劃分出來。假設Java堆中記憶體時絕對規整的,所有用到的記憶體在一邊,空閑的記憶體在另一邊,中間放着一個指針作為

分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種配置設定方式成為“指針碰撞”。如果Java堆中的

記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交錯,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上那些記憶體塊是可

用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄,這種配置設定方式稱為“空閑清單”。選擇哪種配置設定方式有Java

堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。是以,在使用Serial、ParNew等帶Compact過程的收集器

時,系統采用的配置設定算法是指針碰撞,而使用CMS這種基于Mark_sweep算法的收集器時,通常采用空閑清單。

除如何劃分可用空間外,還有另外一個需要考慮的問題是對象建立在虛拟機中是非常頻繁的操作,即使是僅僅修改一個指針所指向的位置,在并發情況

下是線程不安全的,可能出現正在給對象A配置設定記憶體,指針還沒有來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。解決這個問題有兩

種方案,一種是對配置設定記憶體空間的動作進行同步處理——實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把記憶體配置設定動作

按照線程劃分在不同的記憶體空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定緩沖(Thread Local Allocation Buffer ,

TLAB)。那個線程要配置設定記憶體,就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時才需要同步鎖。虛拟機是否使用TLAB,可以通過-

XX:+/-UseTLAB參數來設定。

記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB配置設定時進行。

這一步操作保證了對象的執行個體字段在Java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型對應的零值。接下來,虛拟機要對對

象進行必要的設定,例如這個對象是那個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊。這些資訊存放在對象

的對象頭之中。根據虛拟機目前的運作狀态的不同,如是否啟用偏向鎖等,對象頭會有不同的設定方式。上面工作都完成後,從虛拟機的角度來看,一

個新的對象已經誕生了,但從Java程式來說,對象建立才剛剛開始,所有的字段都還為零,需要進行一些初始化操作。

總結,對象的建立虛拟機首先需要進行類加載檢查,檢查通過之後,根據類加載完成後确定的記憶體大小,為對象配置設定記憶體;接着,需要對配置設定到的記憶體

空間都初始化為零值;然後,虛拟機要對對象設定一些基本資訊,如對象是那個類的執行個體、對象的哈希碼、對象的GC分代年齡資訊、如何才能找到類

的中繼資料資訊等,到這裡虛拟機建立對象的工作已經完成;最後,從程式的角度,我們還需要對對象進行初始化操作。

二、 對象的記憶體布局

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

HotSpot虛拟機的對象頭包括兩部分資訊,第一部分用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有

的鎖、偏向線程ID、偏向時間戳等,這部分資料長度在32位和64位的虛拟機中分别為32bit和64bit,官方稱它為“Mark Word”。對象需要存儲的運作時

資料很多,其實已經超出了32位、64位Bitmap結構能夠記錄的限度。但是對象頭資訊是與對象自身定義的資料無關的額外存儲成本,考慮到虛拟機的

空間效率,Mark Word被設計成為一個固定的資料結構以便在極小的空間存儲盡量多的資訊,它會根據對象的狀态複用自己的存儲空間。

對象頭的另外一個部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體,并不是所有的虛拟機實作

都必須在對象資料上保留類型指針(還有通過句柄的方式)。另外,如果對象是一個Java數組,拿在對象頭中還必須有一塊用于記錄數組長度的數

據,因為虛拟機可以通過普通Java對象的中繼資料資訊确定對象的大小,但是從數組的中繼資料中卻無法确定數組的大小。

接下來的執行個體資料部分是對象真正存儲的有限資訊,也是程式代碼中所定義的各種類型字段内容。無論是從父類繼承下來的,還是在子類中定義的,都

需要記錄起來。這部分的存儲順序會受到虛拟機配置設定參數(FieldAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛拟機預設的配置設定策

略為long/doubles、int、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers),從配置設定政策中可以看出,相同寬度的字段總是被配置設定到一

起。在滿足這個前提的條件下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值為true,那麼子類之中較窄的變量也可能會插入到

父類變量的空隙之中。

第三部分對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8

位元組的整數倍,換句話說,就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數,是以,當對象執行個體資料部分沒有對齊時,就需

要通過對齊填充來補全。

三、 對象的通路定位

建立對象是為了使用對象,我們的Java程式需要通過棧上的reference資料來操作堆上的具體對象。由于reference類型在Java虛拟機規範中隻規定了一

個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、通路堆中的對象的具體位置,是以對象通路方式也是取決于虛拟機實作而定的。目

前主流的通路方式有使用句柄和直接指針兩種。

如果使用句柄通路的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。

對象的建立、記憶體布局、對象的通路定位
通過句柄通路對象
如果是直接指針通路,那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference中存儲的直接就是對象位址。
對象的建立、記憶體布局、對象的通路定位
   通過直接指針通路對象

這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象呗移動(垃圾收集時移動對象是非常普

遍的行為)是隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定

位的時間開銷,由于對象的通路在Java中非常頻繁。Sun HotSpot虛拟機采用的是第二種方式。

參考文獻:

《深入了解Java虛拟機》