Java 程序員都該懂的 JVM 內存區域

對於 Java 程序員來說,在 JVM 自動內存管理機制的幫助下,不再需要爲每一個 new 操作去寫對應的 delete/free 代碼,不容易出現內存泄露和內存溢出的問題。不過正因如此,如果不瞭解虛擬機是怎樣使用內存的,一旦出現內存泄露和內存溢出的問題,那麼排查錯誤將會非常艱難。

一. 內存區域


虛擬機在執行 Java 程序的過程中會把它管理的內存劃分爲若干個不同的數據區域,這些區域各司其職

1. 線程私有

下面這 3 個區域都是線程私有的區域,每個線程獨佔一份

(1)程序計數器

  • 當前線程所執行的字節碼的行號指示器
  • 通過改變計數器的值來選取下一條執行的字節碼指令
  • 幫助完成分支,循環,跳轉,異常處理,線程恢復等基礎功能
  • 多線程環境中,爲了正常完成線程的切換,使得各個線程能恢復到正確的執行位置,因此每條線程都需要一個程序計數器
  • 唯一一個不會出現 OutOfMemoryError 情況的區域

(2)虛擬機棧

  • 每個方法執行時都會創建一個棧幀,當方法被調用,棧幀入棧,方法執行完成,出棧
  • 每個棧幀存儲局部變量表,操作棧,動態鏈接,方法出口等
  • 局部變量表存儲了當前方法的局部變量,包括基本數據類型,對象引用(指針)以及 returnAddress 類型(指向一條字節碼指令的地址)
  • 可能出現的兩種異常:
    StackOverflowError:棧溢出,線程請求的棧深度大於虛擬機允許的深度
    OutOfMemoryError:內存溢出,如果虛擬機棧可以動態擴展,那麼如果擴展時無法申請到足夠的內存,則拋出異常

(3)本地方法棧

  • 作用,運行機制,異常類型等與虛擬機棧相同
  • 爲 native 方法服務
  • 很多虛擬機中,本地方法棧和虛擬機棧合二爲一

2. 線程共享

下面兩個爲線程共享的區域

(4)堆

  • 虛擬機管理的內存中最大的一塊
  • 存放對象實例
  • 垃圾回收器管理的主要區域,也被稱爲 GC 堆,並因此可以細分爲新生代和老年代
  • 可以處於不連續的空間中
  • 當沒有內存完成實例分配,堆也無法擴展時,拋出 OutOfMemoryError

(5)方法區

  • 存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等
  • 垃圾回收行爲在這個區域很少出現,因此也被稱作爲“永久代”

值得注意的是,從 Java 8 開始,方法區被移除,取而代之的是一個叫元空間(Metaspace)的區域

(6)運行時常量池

  • Java 6 及之前屬於方法區的一部分;Java 7 後被移入堆區域
  • 存放編譯器生產的各種字面量和符號引用,在類加載時期存入運行時常量池

二. 瞭解 Java 對象


1. 對象的創建

對象的創建過程中有以下幾大步驟:

(1)類加載檢查

當虛擬機遇到一個 new 指令,說明要創建對象了;但在創建對象之前,會先去檢查這個類是否已經加載過,解析和初始化過,如果沒有,則先執行類加載過程

(2)分配內存

在堆中劃分出一塊確定大小的內存,分配方式有兩種

分配方法

  • 指針碰撞:使用這種方法,堆內存必須是規整的(用過的放一邊,空閒的放一邊),然後中間放一個指針作爲分界點。分配內存時只需要將指針挪一段與對象大小相等的距離即可
  • 空閒列表:如果堆內存不規整,就只能使用空閒列表法了。JVM 維護一個列表來記錄內存塊的使用情況;分配時找到一塊足夠大的空間劃分給對象然後更新表記錄即可

保證線程安全

爲了保證線程安全,避免同一塊區域同時分配給多個對象,通常使用兩種方法

  • CAS:每當要寫入數據時,先比較當前值(工作內存)與主內存中的值是否一致,是則進行寫入,否則重新獲取值
  • TLAB:每個線程預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB);如果某個線程的 TLAB 用完,需要分配新的 TLAB,這些則需要進行同步鎖定

(3)初始化零值

將分配到的內存空間都初始化爲零值;這樣可以使得對象在代碼中不賦初始值就直接使用

public class Person {
    int age;

    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.age);
    }
}

類似這樣的情況,age 被默認賦值爲 0

(4)設置對象頭

將類的自身運行時數據(HashCode,GC 分代年齡,鎖狀態標誌等);類型指針(指向類元數據,確定是哪個類的實例)存放在對象頭中

(5)執行 init 方法

前面幾步完成後,對於虛擬機,一個新對象已經產生了,但對於 java 程序,需要執行 init 方法後,一個真正的對象才完全產生出來

二. 對象的內存佈局

堆裏面存放的是對象實例,對象實例由三個部分組成

  • 對象頭:存儲自身運行時數據(HashCode,GC 分代年齡,鎖狀態標誌等);類型指針,指向類元數據(確定是哪個類的實例)
  • 實例變量:記錄對象的各種屬性數據信息
  • 填充數據:用於對齊字節(JVM 對對象起始地址的字節數有要求,是8字節的整數倍

猜你喜歡

你的關注就是我寫作最大的動力

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章