JVM物件揭祕以及記憶體佈局
下文我將從物件的建立和物件在記憶體中的佈局兩個方面介紹
1.物件的建立
java是一門面向物件的語言,java的世界裡,無時無刻都有新的物件產生。在java語法層面上來看,新建一個物件就是java裡面的new關鍵字。那麼對映到JVM裡面,物件是如何建立的呢。
首先虛擬機器會檢視新建立的物件是否可以在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已經經過類的載入,解析,初始化的過程。如果沒有,那麼就必須進行類的載入等過程(這個以後會詳細講,主要就是把一個編譯後的class檔案經過一系列的檢查和解析,然後把class檔案解析後的資料載入到虛擬機器的過程,這步會把常量放到常量池裡面,並且在方法去儲存類的一些符號引用,符號引用就是引用的字母,這部分我還在研究中,以後我會單獨來寫類的載入過程)。經過類的載入之後,就要真正的建立物件,首先需要做的就是給物件分配記憶體,而物件所需要的記憶體大小在類的載入過程中就會確定。
前面講過,物件都是儲存在堆裡面的,而虛擬機器是允許系統給虛擬機器分配的堆是不連續的記憶體空間的。那麼分配的記憶體的時候就會遇見兩種不同的情況。
①如果記憶體是連續的。就是用過的記憶體在一邊,沒有用過的在另一邊,中間有一條邏輯上的分界線。那麼分配記憶體時只需要把分界線向空閒的方向移動物件的大小的距離就可以了。這種方式稱為指標碰撞。
②如果記憶體是不連續的。就是用過的記憶體和沒有用過的記憶體是交叉在一期的。這個時候虛擬機器就需要知道哪些記憶體空間是空閒的,就是虛擬機器要維持一個空閒列表記錄空閒的記憶體區域。然後找到合適的記憶體分配給物件後,更新空閒列表,這種分配記憶體的方式成為空閒列表。
具體虛擬機器上使用的是哪種分配方式取決與堆得空間是否連續,而這部分由取決與垃圾回收器是否會在GC之後對記憶體進行整理。如果回收器對記憶體進行了整理,那麼就是用記憶體碰撞,否則就需要使用空閒列表。
java裡面建立物件是很頻繁的操作,那麼如何在這段期間保持原子性呢?第一種解決方式就是CAS加上失敗重試機制,第二種解決方案是TLAB,本地執行緒分配緩衝的方法(這個不是特別理解)。
記憶體分配之後,虛擬機器會把分配的記憶體空間的初始值都設定為零(各個型別對應的零值),除了物件頭。這一部分保證了物件的全域性變數在不進行的初始化的時候也可以訪問。最後就是根據程式碼上的邏輯給不同的全域性變數賦上對應的初始值了,這樣一個物件(程式設計師的女朋友)就被創建出來了。
2.物件的記憶體佈局
在預設的HotSpot虛擬機器上面,物件的記憶體通常分為三部分
物件頭 | ObjectHeader |
例項資料 | instancedata |
對齊填充 | padding |
①物件頭
物件頭包含兩部分資訊,一部分是儲存物件自身執行時的資料,包括GC年齡,是否持有鎖,雜湊碼等資訊。
還有一部分就是儲存的物件的一些靜態資訊,比如型別指標,就是物件指向他的類元資料的指標。虛擬機器通過這個指標找到這個物件所屬類的元資料資訊。但是這個並不是固定的。這就涉及到物件的訪問定位的方式問題。通產物件頭的大小在32bit的虛擬機器上大小就是32bit,在64bit虛擬機器上就是64bit。注意如果這個物件是陣列,那麼一般還需要額外的32bit來記錄陣列的長度,因為根據陣列的元資料無法去確認陣列的大小。
-
在32位系統下,存放Class指標的空間大小是4位元組,MarkWord是4位元組,物件頭為8位元組。
-
在64位系統下,存放Class指標的空間大小是8位元組,MarkWord是8位元組,物件頭為16位元組。
-
64位開啟指標壓縮的情況下,存放Class指標的空間大小是4位元組,MarkWord是8位元組,物件頭為12位元組。
-
陣列長度4位元組+陣列物件頭8位元組(
物件引用4位元組(未開啟指標壓縮的64位為8位元組)+陣列markword為4位元組(64位未開啟指標壓縮的為8位元組)
)+對齊4=16位元組。 -
靜態屬性不算在物件大小內。
②例項資料
例項資料儲存的就是類本身的資料,一般情況來說就是類的成員變數。無論是從父類繼承下來的還是本身的欄位,都需要記錄。這部分除了跟欄位在程式碼中定義的順序有關之外,還跟虛擬機器的分配策略引數有關係。一般情況下,都是父類的引數在子類的前面,相同寬度的欄位總是分配到一起(比如boolean和byte,double和long會優先分配到一起),還有就是會優先分配寬度較小的欄位,這個主要是為了節約記憶體,跟補齊區域有關係。注意子類較小寬度的變數也有可能插在父類的變數之間。
③對齊填充
這部分割槽域並不是必然的。主要是以為HotSpot記憶體管理系統要求物件的記憶體之和需要時8位元組的整數倍,所以在前兩部分沒有達到要求的時候便會出現對齊填充區域。
3.物件的訪問方式
由於棧上的引用型別資料只是存了堆上資料的引用,但時java虛擬機器規範並沒有規定如何根據引用定位到具體的物件和找到物件的一些元資料。所以這個完全是根據虛擬機器本身來實現的。現在市主要有兩種訪問方式
①.根據控制代碼訪問:
可以看到堆裡面需要維護一個控制代碼池,一個指向例項資料,一個指向物件所屬類的元資料。棧上面的引用只是應用到控制代碼池就可以。
②根據指標訪問
可以看到只有一個指標指向物件的實際記憶體區域,然後在根據物件的實際記憶體區域指向所屬類的元資料。
上面兩種方式各有優點和缺點。控制代碼訪問好處是引用型別的控制代碼比較穩定,即使物件發生變化,也只是改變控制代碼池裡面控制代碼的指向就可以,不需要改變引用型別的控制代碼,但是指標訪問就需要改變引用型別的指標。但是它的確定也很明顯,就是定位的時候對多一次控制代碼定位的開銷。效率沒有指標訪問快。但是java是一個物件建立十分頻繁的語言,所以對定位速度要求比較高,所以大部分虛擬機器都是採用的指標訪問。