前言
上篇文章介紹了Java虛擬機器的執行時資料區域,大致明白了Java虛擬機器記憶體模型的概況,下面就基於實用優先的原則,以最常用的虛擬機器HotSpot和最常用的記憶體區域Java堆為例,升入探討一下HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。
物件的建立
Java是一門物件導向的程式語言,在Java程式的執行過程中每時每刻都有物件被建立出來,那麼在虛擬機器中,物件的建立是怎樣的一個過程呢?
當Java虛擬機器遇到一條位元組碼new指令時,首先檢查這個指令的引數是否能定位到一個類的符號引用,然後檢查這個類是否已經被載入、解析和初始化過。如果沒有,那麼先執行型別的載入過程。
為物件分配空間
在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。為物件分配空間的任務實際上便等同於把一塊確定大小的記憶體塊兒從Java堆中劃分出來。
在解釋Java堆是如何為物件分配空間的時候,先解釋兩個虛擬機器常用的分配空間方式。
- 指標碰撞
當一塊兒記憶體中的空間是絕對規整的時候,就是說,所有被使用過的記憶體放在一邊,空閒的記憶體放在另一邊,中間放著一個指標,作為分界點的指示器,當分配記憶體是,就僅僅是把指標向空閒的方向挪動一段與物件大小相等的距離,這種分配方式稱為“指標碰撞”(Bump The Pointer)。 - 空閒列表
當一塊兒記憶體的空間不是規整的時候,已被使用的記憶體和空閒的記憶體相互交錯在一起,那就沒辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表”(Free List)。
具體選擇哪種分配方式,是由Java堆中的記憶體空間是否規則來決定的,而Java堆是否規整有由所採用的垃圾收集器是否帶有空間壓縮整理的能力決定。所以,當使用Serial、ParNew等帶壓縮整理過的的收集器是,物件的分配方式是指標碰撞,而當使用CMS這種基於清除演算法的收集器是,理論上就只能採用較為複雜的空閒列表來分配記憶體。
物件建立的執行緒安全
在物件建立的時候,除了如何劃分可用空間外,還有一個問題,那就是在分配記憶體空間的時候如何保證執行緒安全。
解決這個問題有兩種方案:
- 一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機器是採用CAS配上失敗重試的方式保證更新操作的原子性;
- 另外一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allcation Buffer,TLAB),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩衝區中分配,只有本地緩衝區用完了,分配新的緩衝區時,才需要同步鎖定。
在保證了執行緒安全的為物件分配了記憶體空間後,從虛擬機器的視角來看,一個新的物件已經產生了。
但是從Java程式的視角看來,物件建立才剛剛開始,建構函式,也就是Class檔案中的< init >方法還沒有執行,new 指令之後會執行< init >方法,
按照程式設計師的意願對物件進行初始化,這樣一個真正可用的物件才算完全被構造出來。
物件的記憶體佈局
在HotSpot虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分為三個部分:物件頭(Header)、例項資料(Insetance Data) 和對齊填充(Padding)。
物件頭
HotSpot虛擬機器物件的物件頭包括兩類資訊。
- 第一類是用於儲存物件自身執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌等。這部分資料的長度在32位和64位的虛擬機器中分別為32個位元和64個位元,稱為 “ Mark Word ” 。
- 物件頭的另外一部分是型別指標,即物件指向它的型別後設資料的指標,Java虛擬機器通過這個指標來確定該物件是哪個類的例項。如果物件是一個Java陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料,因為需要通過陣列的長度來確定物件的大小。
例項資料
例項資料是物件真正儲存的有效資訊,即我們在程式程式碼裡面所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來。這部分的儲存順序會受到虛擬機器分配策略引數(-XX:FieldsAllocationStyle引數)和欄位在Java原始碼中定義順序的影響。
對齊填充
由於HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是任何物件的大小都必須是8位元組的整數倍。物件頭部分已經被精心設計成正好是8位元組的倍數(1倍或2倍),因此如果物件例項資料部分沒喲對齊的話就需要通過對齊填充來補全。所以對齊填充為,並不是必然存在的一部分佔位符。
物件的訪問定位
物件建立完成後就可以使用了,物件的定位是根據棧中的引用資料,來確定物件在記憶體中的位置的。那麼如何通過引用資料定位到堆中的物件位置呢?
主流的訪問方式主要有使用控制程式碼和直接指標兩種:
- 如果使用控制程式碼訪問的話,Java堆中將可能會劃分出一塊記憶體來作為控制程式碼池,引用資料中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自具體的地址資訊。
- 如果使用直接指標訪問的話,Java堆中物件的記憶體佈局就必須考慮如何放置訪問型別資料的相關資訊,引用資料中儲存的直接就是物件地址,這樣訪問物件更快捷。
使用控制程式碼,在物件被移動(垃圾收集時)時只會改變控制程式碼中的資料指標,而直接指標節省了一次指標定位的時間開銷速度更快!