探秘虛擬機對象
Java是一門面向對象的編程語言,在實際開發中,我們與對象打交道是最頻繁的,但是我們對Java中的對象又了解多少呢?我們創建的對象又是如何與虛擬機交互的?本文將介紹闡述對象是如何被虛擬機創建的,對象的內存布局又是怎麼樣的,以及如何訪問和定位對象。
本文大綱:
1、對象的創建
2、對象的內存布局
3、對象的訪問定位
4、總結
一、對象的創建
1、判斷類是否被載入、解析、初始化過
在Java程序運行過程中無時無刻都有對象被創建,在語言層面上,創建對象通常是使用new關鍵字,如new Person(「小明」);克隆和反射也會創建對象。
但是在虛擬機中,遇到一條new指令時,首先會檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,常量池位於虛擬機的方法區中,類的符號引用包含的信息可以簡單理解為類的全限定名,符號引用的細節可以查看我之前寫的編號為【0016】的類文件結構文章。
言歸正傳,虛擬機檢查類的符號引用可以判斷類是否被載入、解析和初始化過,如果沒有,則執行類的載入過程。對於類的載入過程,有興趣可以查看編號【0014】的文章。
2、為新對象分配內存
檢查完類載入之後,虛擬機會為我們創建的對象分配內存空間。分配的方式有指針碰撞和空閑列表兩種,具體選用哪種跟我們虛擬機的垃圾收集演算法有關。
指針碰撞:如果虛擬機中的堆內存是絕對規整的,所有用過的內存在一邊,沒有用過的放在另外一邊,中間放著一個指針作為分界點的指示器,分配內存時把那個指針向空閑的空間那邊挪動一段與對象大小相等的距離,這種分配方式就是「指針碰撞」。
空閑列表:當堆內存是不規整時,為了區分那些內存是可用的,那些是不可用的,需要用一個列表來記錄,而這個列表被稱為空閑列表,記錄堆內存的使用情況,當為對象分配空間時,從空閑列表中尋找出一塊與對象相等大小的空閑空間,並記錄下新的使用情況。
確定了如何劃分內存空間之後,還有一個問題就是,對象的創建在虛擬機中是非常頻繁的行為,比如,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況,解決這種並發問題,一般有兩種方案:
方案一:對分配內存空間的動作進行同步處理,比如,虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性。
方案二:把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩衝(ThreadLocal Allocation Buffer,TLAB),哪個線程要分配內存,就在哪個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定,虛擬機是否使用TLAB,可以通過-XX:+/-UserTLAB參數來設定。
3、初始化分配到的內存空間
內存分配完成後,虛擬機將分配到的內存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作也可以提前至TLAB分配時進行。也正是這一步操作,才保證了我們對象的實例欄位在Java代碼中可以不賦初值就直接使用。
4、設置對象的對象頭信息
上面工作完成後,虛擬機對對象進行必要的設置,主要是設置對象的對象頭信息,比如,這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等。
5、執行對象初始化方法
其實,上面工作完成後,從虛擬機角度來看,一個新的對象已經產生了,但從Java程序的角度來看,對象創建才剛剛開始,對象實例中的欄位僅僅都為零值,還需要通過方法進行初始化,按照程序員的意願對對象進行初始化。此時,一個真正可用的對象才算完全產生出來。方法包括執行對象的構造方法,初始化實例欄位。
二、對象的內存布局
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為三塊區域:對象頭、實例數據和對齊填充。
1、對象頭(Header)
對象頭包括兩部分,第一部分是對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖等。
第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象時哪個類的實例。類元數據包括:類的版本號、類的全限定名、類的靜態屬性、類的訪問標誌等。類的元數據位於方法區,不在堆。
2、實例數據(Instance Data)
對象的實例數據主要存儲的是各種類型的欄位內容,無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。
3、對齊填充(Padding)
由於大部分虛擬機的自動內存管理系統要求對象的起始地址必須是8位元組的整數倍,所以如果對象的大小剛好不是整數倍,就要進行填充。如果剛好是整數倍,則不需要填充。
三、對象的訪問定位
程序執行時,虛擬機會為每個線程分配一個棧,每個方法就是一個棧幀,棧中存儲了變數的地址(reference),我們對數據的使用是通過棧上的reference數據來操作堆上的具體對象,對於不同的虛擬機實現,reference數據類型有不同的定義,主要有兩種訪問方式:
1、句柄
使用句柄訪問的方式,Java堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
2、直接指針
如果使用直接指針的方式來訪問,Java堆對象的布局就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。
優劣勢對比:
使用句柄的方式最大的好處就是reference中存儲的就是穩定的句柄地址,在對象被移動(垃圾收集器移動對象時非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改。
使用直接指針訪問的方式,最大好處就是速度更快,節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此可以節省執行成本。
我們開發中大多數是使用Sun的HotSpot虛擬機,它是使用直接指針的方式進行對象訪問的,但是一些框架使用句柄的方式也不少,如動態代理技術。
四、總結
簡單總結一下,本文主要介紹了虛擬機對象的創建,創建的過程大致分為五個步驟:1、判斷類是否被載入、解析以及初始化過。2、為新對象分配內存。3、初始化分配到的內存空間。4、設置對象的對象頭信息。5、執行對象初始化方法。同時,也介紹了對象在虛擬機中的內存布局,包括對象頭、實例數據以及對齊填充。最後,介紹了對象的訪問方式有句柄和直接指針兩種。感謝你的閱讀,謝謝。


TAG:全球大搜羅 |