虛擬機器執行時資料區域描述了虛擬機器管理的記憶體劃分情況,但是目前我們對於虛擬機器還是有很多困惑,比如:
- 問題1:如何為物件分配記憶體?
- 問題2:物件記憶體模型是怎樣的?
- 問題3:是怎樣訪問記憶體中的物件的?
- 問題4:分配記憶體的時候如果遇到併發問題,怎麼保證分配操作的執行緒安全性?
為了搞清楚這些問題,我們先從虛擬機器是如何建立物件開始講起。
一、物件建立過程
當虛擬機器遇到一條new 指令時,便會進行物件的建立過程。
建立物件的過程如下:
- 1.檢查常量池中有沒有這個類的符號引用,並且檢查這個符號引用代表的類有沒有被虛擬機器載入過。
如果沒有被載入過,則執行類載入過程,然後進入下一步; 如果已載入,則進入下一步。
- 2.根據方法區中類的資訊,在堆區劃分一塊確定大小的記憶體給物件。
(經過類載入後,類的資訊被儲存在方法區中,一個類的物件所需的記憶體大小也固定下來。)
- 3.為物件的成員變數賦初始值
記憶體分配完成之後,需要對分配的記憶體空間部分割槽域的內容都初始化為零值。 這一步保證了物件成員變數在java程式碼中可以不賦初始值。
-
4.設定物件頭中的資訊
關於物件頭是什麼, 別急,繼續往下看。
-
5.呼叫
<init>
方法進行初始化別再問
<init>
是什麼了,先往下看。
二、問題1解惑:
在堆區分配記憶體有兩種方式。
- 指標碰撞法
如果堆中記憶體是規整的,即所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間用一個指標做分界點的指示器。
分配記憶體的過程,實際上就是指標向空閒空間那邊移動一段與物件大小相等的距離。
- 空閒列表法
java堆中的記憶體如果不是規整的,就需要使用空閒列表的分配方式。
空閒列表概念:虛擬機器維護了一個列表,用於記錄哪些記憶體塊是可用的。
在分配的時候,從列表中找到一塊滿足物件大小的記憶體空間劃分給物件例項,同時會更新列表上的記錄。
- 關於兩種分配方式的選擇
選擇哪種分配方式取決於java堆是否規整。
而java堆是否規整取決於所採用的垃圾收集器是否帶有壓縮整理的功能。
因此,選擇哪種分配方式最終取決於使用了哪種垃圾收集器。
- 使用了指標碰撞的垃圾收集器有哪些?
serial、ParNew等基於複製演算法或標記整理(Mark Compact)演算法的收集器,不會導致記憶體碎片,因此使用的是指標碰撞。
- 採用空閒連結串列垃圾收集器有哪些?
CMS等基於Mark-Sweep(標記清除)演算法的收集器,會產生記憶體碎片,所以使用空閒列表法。
三、問題2解惑
物件在記憶體中的資料除了例項本身的資料外,還包括物件頭和對齊填充
3.1 例項資料
例項資料儲存的是成員變數的值,包括從父類繼承下來的成員變數。
成員變數在記憶體中的順序:相同寬度的欄位會分配在一起,父類定義的變數會出現在子類之前, 預設情況下,子類中較窄的變數可能會被插入到父類變數的間隙中。反正就是不一定按定義的順序來分配。
3.2 物件頭是什麼?
物件頭的作用是記錄物件在執行過程中所需的資料。
比如物件屬於哪個類的例項、所屬類的資訊在方法區中的位置(型別指標)、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊就儲存在物件頭中(Object Header)
3.3 對齊填充又是什麼?
對齊填充是用於確保物件的記憶體的總長度為8位元組的整數倍。
為什麼要是確保是8位元組的整數倍呢?
因為hotspot要求物件起始地址為8位元組的整數倍以便於自動記憶體管理, 換句話說,物件的總長度要為8位元組的整數倍才能保證如此。 而又因為物件頭正好是8位元組(32位或64位)的整數倍,但是例項資料長度是任意的,因此需要對齊補充來確保整個物件總長度為8位元組的整數倍。
四、問題3解惑
java程式需要通過引用來操作堆上的具體資料。 根據引用存放的地址型別的不同,物件有不同的訪問方式
主要有兩種訪問方式:
- 使用控制程式碼訪問
- 使用直接指標訪問
4.1 使用控制程式碼訪問
堆中會劃分一塊記憶體用來做控制程式碼池。引用中儲存的就是物件的控制程式碼地址。控制程式碼包含了物件例項資料和物件型別的資料的指標。
通過引用訪問物件的時候,會首先根據引用找到物件的控制程式碼,然後根據控制程式碼中物件的地址來訪問物件。
4.2 使用直接指標訪問
引用中儲存的直接是物件的地址,直接通過引用來訪問物件。
4.3 兩種方式對比
-
使用控制程式碼
- 優點:引用中儲存的是穩定的控制程式碼地址,發生垃圾收集時可能會移動物件,這時候只需要改變控制程式碼中例項資料的指標指向新物件,而引用的值不需要改。
- 缺點:需要兩次定址。
-
使用直接指標
- 優點:速度快,一次定址即可。
- 缺點:需要在物件例項的記憶體中儲存一個指向方法區中該型別資料的指標。不過使用控制程式碼方式控制程式碼中也需要儲存型別指標。
直接指標的速度快,hotspot採用就是直接指標的方式
五、問題4解惑
物件分配記憶體不是執行緒安全的,比如給物件A分配記憶體,還沒來得及修改指標的指向, 另一個執行緒建立物件B也用了原來的指標,這樣就會出問題的。
如何解決?
- 方案1: 對分配記憶體空間的動作進行同步處理
實際上虛擬機器採用CAS配上失敗重試的方式保證更新指標操作的原子性。
- 方案2:把記憶體分配的動作按照執行緒劃分在不同的空間中進行
即:每個執行緒在java堆中預分配一小塊記憶體, 這一小塊記憶體稱作“本地執行緒分配緩衝"(Thread Local Allocation Buffer, TLAB)
記憶體分配的過程就可以總結為:不同執行緒使用指標碰撞或者空閒列表的方式在各自的TLAB
上分配記憶體。
當執行緒的TLAB
用完需要分配新的TLAB
,這時候才需要同步記憶體分配操作。
虛擬機器是否需要使用TLAB
,可以通過-XX:+/-UseTLAB
引數來決定。
六、遺留問題:<init>方法是個啥?
從上面物件的建立過程,我們可以瞭解到,在記憶體分配完成之後,所有成員變數的值都還只是零值。
對於虛擬機器來說,物件建立已經完畢,但是,對於java程式來說,物件的初始化才剛開始。
成員變數的初始化工作交由<init>
方法的來完成。
編譯器收集了成員變數上的賦值操作,例項初始化程式碼塊的賦值操作,以及構造方法中的賦值操作,構成了<init>
方法,並執行,物件就得到了初始化。
學習過java基礎的人都知道,物件初始化的順序為: 成員變數上的賦值-->例項初始化塊-->構造方法。
<init>
方法就解釋了為什麼是這個過程。
七、講點物件頭
物件頭的記憶體模型分三部分:
- Mark Word
- 型別指標
- 記錄陣列的長度
7.1 Mark Word
存放hashCode、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒id、偏向時間戳等。 長度為32位或者64位(32位虛擬機器和64位虛擬機器)。
mark word是一個非固定的資料結構,在不同情況下結構會有所變化。
比如:在32位的虛擬機器中,如果物件處於未被鎖定的狀態, mark Word的32位空間將有25位用於儲存hashcode, 4位用於儲存物件的分代年齡,2位用於儲存物件上鎖 標誌,1位固定為0
這些東西我就不一個個介紹他們是用來幹嘛的,講多了反而複雜,大概瞭解就行,有興趣的可以百度。
7.2 型別指標
一個指向類後設資料的指標,通過這個指標,可以確定物件是哪個類的例項。記住,這個指標是在物件頭中,但不是在Mark Word中的。
7.3 陣列長度
如果物件是一個陣列,在物件頭中還必須有一塊用於記錄陣列長度的資料。
這一部分僅在物件是陣列的時候存在。
點贊是對我最大的鼓勵