【深刻學習JVM 02】HotSpot虛擬機對象探祕

虛擬機運行時數據區域描述了虛擬機管理的內存劃分狀況,可是目前咱們對於虛擬機仍是有不少困惑,好比:java

  • 問題1:如何爲對象分配內存?
  • 問題2:對象內存模型是怎樣的?
  • 問題3:是怎樣訪問內存中的對象的?
  • 問題4:分配內存的時候若是遇到併發問題,怎麼保證分配操做的線程安全性?

爲了搞清楚這些問題,咱們先從虛擬機是如何建立對象開始講起。算法

1、對象建立過程

當虛擬機遇到一條new 指令時,便會進行對象的建立過程。數組

建立對象的過程以下:安全

  • 1.檢查常量池中有沒有這個類的符號引用,而且檢查這個符號引用表明的類有沒有被虛擬機加載過。

若是沒有被加載過,則執行類加載過程,而後進入下一步; 若是已加載,則進入下一步。數據結構

  • 2.根據方法區中類的信息,在堆區劃分一塊肯定大小的內存給對象。

(通過類加載後,類的信息被保存在方法區中,一個類的對象所需的內存大小也固定下來。)併發

  • 3.爲對象的成員變量賦初始值

內存分配完成以後,須要對分配的內存空間部分區域的內容都初始化爲零值。 這一步保證了對象成員變量在java代碼中能夠不賦初始值。學習

  • 4.設置對象頭中的信息線程

    關於對象頭是什麼, 別急,繼續往下看。3d

  • 5.調用<init>方法進行初始化指針

    別再問<init>是什麼了,先往下看。

2、問題1解惑:

在堆區分配內存有兩種方式。

  • 指針碰撞法

若是堆中內存是規整的,即全部用過的內存都放在一邊,空閒的內存放在另外一邊,中間用一個指針作分界點的指示器。

分配內存的過程,實際上就是指針向空閒空間那邊移動一段與對象大小相等的距離

  • 空閒列表法

java堆中的內存若是不是規整的,就須要使用空閒列表的分配方式。

空閒列表概念:虛擬機維護了一個列表,用於記錄哪些內存塊是可用的。

在分配的時候,從列表中找到一塊知足對象大小的內存空間劃分給對象實例,同時會更新列表上的記錄

  • 關於兩種分配方式的選擇

選擇哪一種分配方式取決於java堆是否規整。

而java堆是否規整取決於所採用的垃圾收集器是否帶有壓縮整理的功能。

所以,選擇哪一種分配方式最終取決於使用了哪一種垃圾收集器

  • 使用了指針碰撞的垃圾收集器有哪些?

serial、ParNew等基於複製算法或標記整理(Mark Compact)算法的收集器,不會致使內存碎片,所以使用的是指針碰撞。

  • 採用空閒鏈表垃圾收集器有哪些?

CMS等基於Mark-Sweep(標記清除)算法的收集器,會產生內存碎片,因此使用空閒列表法。

3、問題2解惑

對象在內存中的數據除了實例自己的數據外,還包括對象頭對齊填充

3.1 實例數據

實例數據存儲的是成員變量的值,包括從父類繼承下來的成員變量。

成員變量在內存中的順序:相同寬度的字段會分配在一塊兒,父類定義的變量會出如今子類以前, 默認狀況下,子類中較窄的變量可能會被插入到父類變量的間隙中。反正就是不必定按定義的順序來分配。

3.2 對象頭是什麼?

對象頭的做用是記錄對象在運行過程當中所需的數據

好比對象屬於哪一個類的實例、所屬類的信息在方法區中的位置(類型指針)、對象的哈希碼、對象的GC分代年齡等信息。這些信息就保存在對象頭中(Object Header)

3.3 對齊填充又是什麼?

對齊填充是用於確保對象的內存的總長度爲8字節的整數倍。

爲何要是確保是8字節的整數倍呢?

由於hotspot要求對象起始地址爲8字節的整數倍以便於自動內存管理, 換句話說,對象的總長度要爲8字節的整數倍才能保證如此。 而又由於對象頭正好是8字節(32位或64位)的整數倍,可是實例數據長度是任意的,所以須要對齊補充來確保整個對象總長度爲8字節的整數倍。

4、問題3解惑

java程序須要經過引用來操做堆上的具體數據。 根據引用存放的地址類型的不一樣,對象有不一樣的訪問方式

主要有兩種訪問方式:

  • 使用句柄訪問
  • 使用直接指針訪問

4.1 使用句柄訪問

堆中會劃分一塊內存用來作句柄池。引用中存儲的就是對象的句柄地址。句柄包含了對象實例數據和對象類型的數據的指針。

經過引用訪問對象的時候,會首先根據引用找到對象的句柄,而後根據句柄中對象的地址來訪問對象。

4.2 使用直接指針訪問

引用中存儲的直接是對象的地址,直接經過引用來訪問對象。

4.3 兩種方式對比

  • 使用句柄

    • 優勢:引用中存儲的是穩定的句柄地址,發生垃圾收集時可能會移動對象,這時候只須要改變句柄中實例數據的指針指向新對象,而引用的值不須要改。
    • 缺點:須要兩次尋址。
  • 使用直接指針

    • 優勢:速度快,一次尋址便可。
    • 缺點:須要在對象實例的內存中保存一個指向方法區中該類型數據的指針。不過使用句柄方式句柄中也須要保存類型指針。

直接指針的速度快,hotspot採用就是直接指針的方式

5、問題4解惑

對象分配內存不是線程安全的,好比給對象A分配內存,還沒來得及修改指針的指向, 另外一個線程建立對象B也用了原來的指針,這樣就會出問題的。

如何解決?

  • 方案1: 對分配內存空間的動做進行同步處理

實際上虛擬機採用CAS配上失敗重試的方式保證更新指針操做的原子性。

  • 方案2:把內存分配的動做按照線程劃分在不一樣的空間中進行

即:每一個線程在java堆中預分配一小塊內存, 這一小塊內存稱做「本地線程分配緩衝"(Thread Local Allocation Buffer, TLAB)

內存分配的過程就能夠總結爲:不一樣線程使用指針碰撞或者空閒列表的方式在各自的TLAB上分配內存。

當線程的TLAB用完須要分配新的TLAB,這時候才須要同步內存分配操做。

虛擬機是否須要使用TLAB,能夠經過-XX:+/-UseTLAB參數來決定。

6、遺留問題:<init>方法是個啥?

從上面對象的建立過程,咱們能夠了解到,在內存分配完成以後,全部成員變量的值都還只是零值。

對於虛擬機來講,對象建立已經完畢,可是,對於java程序來講,對象的初始化纔剛開始。

成員變量的初始化工做交由<init>方法的來完成。

編譯器收集了成員變量上的賦值操做,實例初始化代碼塊的賦值操做,以及構造方法中的賦值操做,構成了<init>方法,並執行,對象就獲得了初始化。

學習過java基礎的人都知道,對象初始化的順序爲: 成員變量上的賦值-->實例初始化塊-->構造方法。

<init>方法就解釋了爲何是這個過程。

7、講點對象頭

對象頭的內存模型分三部分:

  • 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 數組長度

若是對象是一個數組,在對象頭中還必須有一塊用於記錄數組長度的數據。

這一部分僅在對象是數組的時候存在。

點贊是對我最大的鼓勵

相關文章
相關標籤/搜索