深入理解JAVA虛擬機- Java內存區域與對象揭祕

此文是對《深入理解JAVA虛擬機》的一點總結,如果想要了解具體細節可以去看原書。

運行時數據區域

Java虛擬機在執行Java程序的過程中會把內存劃分爲若干個不同的數據區域。如下圖所示:
Java虛擬機運行時數據區
程序計數器
程序計數器可以看作是當前線程所執行的字節碼的行號指示器。在Java虛擬機的概念模型裏,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基本功能都依賴這個計數器來完成。

虛擬機棧
虛擬機棧描述的是Java方法執行的線程內存模型:每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀用於存儲局部變量表(基本類型、對象引用等)、操作數棧、動態連接、方法出口等信息。

本地方法棧
本地方法棧與虛擬機棧發揮的作用是非常相似的,其區別只有虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。甚至有的虛擬機,比如HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二爲一。

Java堆
Java堆是虛擬機管理內存中最大的一塊,是被所有線程共享的一塊內存區域。此內存區域唯一目的就是存放對象實例。在《Java虛擬就規範》中對Java堆的描述是“所有的對象實例以及數組都應當在堆上分配”,而隨着Java語言的發展已經導致一些微妙的變化悄然發生,所以說Java對象實例都分配在堆上也變的不是那麼絕對了。

方法區
方法區用於存儲被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

運行時常量池
運行時常量池是方法區的一部分。常量池表用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。

直接內存
直接內存並不是虛擬機運行數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域。但是這部分內存也被頻繁的使用,而且也可能導OutOfMemoryError異常,所以放到一起講解。
在JDK1.4中新加入了NIO類,引入了一種基於通道與緩衝區的 I/O 方式,可以使用Native 函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏的DirectByteBuffer 對象作爲這塊內存的引用進行操作。

HotSpot虛擬機對象揭祕

這裏以最常用的虛擬機 HotSpot 和最常用的內存區域 堆 爲例,探討一下對象分配、佈局和訪問的全過程。

對象的創建
類加載檢查——>分配內存——>設置零值——>進行必要設置——>初始化init

  1. 類加載檢查

    當虛擬機遇到一條字節碼 new 指令時,首先去檢查能否在常量池中定位到一個類的符合引用,檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

  2. 分配內存

    當類加載檢查通過後,虛擬機將爲新生對象分配內存。根據堆中內存是否規整分爲兩種情況:

  • 假設內存是絕對規整的,所有使用過的內存放在一邊空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒方向挪動一段與對象大小相等的距離,這種分配方式稱爲“指針碰撞
  • 假設內存不是規整的,被使用的內存與空閒內存交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛擬就就必須維護一個列表,記錄內存塊哪些是可用的,分配時,在空閒列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方法稱爲“空閒列表

對象創建在虛擬中是非常頻繁的行爲,哪怕只是修改一個指針所指向的位置,在併發情況下也不是線程安全的。解決這個問題有兩種可選方案:

  • 對分配內存的空間的動作進行同步處理——實際虛擬機是採用CAS配上失敗重試的方式來保證更新操作的原子性。
  • 把內存分配的動作按照線程分在不同的空間進行——每個線程在堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB),線程要分配內存,先在線程的本地緩衝區中分配,當本地緩衝區用完了,分配新的緩存區時才需要同步鎖定。
  1. 初始化零值
    虛擬機將分配到的內存空間都初始化爲零值,這步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數據類型對應的零值。
  2. 進行必要設置
    這個對象是哪個類的實例,如何找到類元數據信息,對象GC分代年齡等信息,這些信息放在對象的對象頭中。
  3. 執行init方法
    在上面的工作都完成後,從虛擬機的視角來看,一個新的對象已經產生了。但是從Java程序的視角來看,對象的創建纔剛剛開始——構造函數(即Class文件中的 init ()方法)還沒有執行,所有字段都是默認的零值。此時按照程序員的意願堆對象進行初始化,這樣一個真正可用的對象纔算完全被構造出來。

對象的內存佈局

在HotSpot虛擬機裏,對象在堆內存中的存儲佈局可以劃分爲3個部分:對象頭、實例數據和對齊填充。

對象的訪問定位

爲了後續使用對象,Java程序會通過棧上的reference數據來操作堆上的具體對象。主流的訪問方式主要有句柄訪問和直接指針兩種:

  • 句柄訪問,在堆中可能會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各種具體的地址信息。
  • 這種實現方法由於用句柄表示地址,因此十分穩定。

在這裏插入圖片描述

  • 直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。
  • 這種方法最大的優勢是速度快,在HotSpot虛擬機中用的就是這種方式。
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章