天天看點

Java記憶體區域與記憶體溢出異常OOM——深入了解Java虛拟機Java記憶體區域與OOM記憶體溢出異常

Java記憶體區域與OOM記憶體溢出異常

一、Java運作時資料區域

Java記憶體區域與記憶體溢出異常OOM——深入了解Java虛拟機Java記憶體區域與OOM記憶體溢出異常

Java虛拟機在執行Java程式的過程中把它管理的記憶體劃分為若幹個資料區域。這些區域有不同的用途、生命周期(建立與銷毀)。根據Java虛拟機規範,JVM所管理的記憶體分為如下幾個部分:

  • 程式計數器
  • Java虛拟機棧
  • 本地方法棧
  • 方法區
  • 運作時常量池
  1. 程式計數器(此記憶體區域無OOM異常)

    程式計數器

    PC Register

    是一塊較小的記憶體區域,可看作是目前線程所執行位元組碼的行号訓示器。位元組碼解釋器工作時通過改變

    PC

    的值選取下一條即将執行的位元組碼指令。

    由于

    Java

    多線程通過線程輪流切換、執行實作,是以為了線程切換之後能恢複到正确的執行位置,每條線程都有自己獨立的一個程式計數器

    PC

    ,這片區域為線程私有的記憶體區域。

    如果線程正在執行

    Java

    方法,

    PC

    記錄的是正在執行的虛拟機位元組碼指令位址;如果正在執行本地

    native

    方法,則

    PC

    的值為空(

    undefined

    )。
  2. Java

    虛拟機棧

    Java

    虛拟機棧是線程私有的,生命周期與線程相同。

    Java

    虛拟機棧描述的是

    Java

    方法執行的記憶體模型。方法在執行的同時(包括方法之間的調用) 會建立棧幀(

    Stack Frame

    ),用于存儲局部變量表、操作數棧、動态連結、方法出口等。每個方法從調用到執行完成,就對應一個棧幀在虛拟機棧中的入棧、出棧操作。

    OOM

    異常:

    (1) 如果線程請求配置設定的棧容量超過

    Java

    虛拟機棧允許的最大容量,抛出

    StackOverflowError

    異常。

    (2) 如果Java虛拟機棧可以動态擴充,如果擴充時無法申請到足夠的記憶體或者建立線程的時候沒有足夠的記憶體空間去建立

    Java

    虛拟機棧,抛出

    OutOfMemoryError

    異常。
  3. 本地方法棧

    Java

    虛拟機棧類似,本地方法棧為虛拟機使用到的

    native

    方法服務。

    OOM

    異常與

    Java

    虛拟機棧類似。

    OOM

    異常:

    (1) 如果線程請求配置設定的棧容量超過

    Java

    本地方法棧允許的最大容量,抛出

    StackOverflowError

    異常。

    (2) 如果本地方法棧可以動态擴充,如果擴充時無法申請到足夠的記憶體或者建立線程的時候沒有足夠的記憶體空間去建立本地方法棧,抛出

    OutOfMemoryError

    異常。
  4. Java

    堆(

    Java Heap

    Java Heap

    JVM

    所管理的記憶體中記憶體最大的一塊。

    Java Heap

    是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。所有的對象執行個體以及數組都在堆上配置設定記憶體。

    從記憶體回收的角度看,由于現代收集器采用分代收集算法,從垃圾回收的角度看,

    Java Heap

    可以分為新生代、老年代。

    Java Heap

    可以處在實體上不連續的記憶體空間中,隻要邏輯上連續就可以。

    OOM

    異常(

    -Xmx堆最大容量

    -Xms

    堆初始化容量):

    (1) 如果在堆中沒有記憶體完成執行個體配置設定,并且堆也無法擴充,抛出

    OutOfMemoryError

    異常。
  5. 方法區(

    Method Area

    )——

    Non-Heap

    (非堆)

    方法區是由各個線程共享的記憶體區域,用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。

    方法區又稱為永久代(

    Permanent Generation

    ),這是由于

    Hotspot VM

    選擇把

    GC

    分代收集擴充至方法區,或者說使用永久代實作方法區。

    OOM

    異常(

    -XX:MaxPermSize

    設定永久代記憶體最大值):

    (1) 當方法區無法滿足記憶體配置設定需求時,抛出

    OutOfMemoryError

    異常。
  6. 運作時常量池(

    Runtime Constant Pool

    )

    是方法區的一部分,

    Class

    檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項資訊就是常量池,用于存放編譯期間生成的各種字面量和符号引用。

    OOM

    異常:

    (1)當建立類或接口的時候,如果構造運作時常量池所需要的記憶體空間超過了方法區所能提供的最大值,抛出

    OutOfMemoryError

    異常。

二、

HotSpot

虛拟機對象建立、布局與通路

1. 對象的建立過程
(1) 虛拟機遇到new指令,先檢查這個指令的參數是否在常量池中定位到一個類的符号引用,并檢查這個符号引用代表的類是否已經被加載、解析和初始化過。如果沒有,先執行類加載。
(2) 為對象進行記憶體配置設定(具體方式由

Java

堆是否規整決定,即由垃圾收集器是否帶有壓縮整理功能決定)

(a) 指針碰撞——所有配置設定的記憶體放在一邊,空閑的記憶體放在另一邊,中間放一個指針作為分界點的訓示器。對象配置設定記憶體就是把指針往空閑區域那邊挪動一段與對象大小相等的距離。

(b) 空閑清單——虛拟機維護一個清單,記錄哪塊記憶體塊可用,在配置設定的時候從清單中選取一塊足夠大的空間配置設定給對象執行個體,并更新清單上的記錄。

注意記憶體配置設定同步問題:(a)對配置設定記憶體空間的動作進行同步(

CAS(Compare And Swap)

+失敗重試) (b)

TLAB

(本地線程配置設定緩沖)
(3) 初始化。對象的記憶體空間配置設定為零值。
(4) 設定對象頭(對象的哈希碼、對象的

GC

分代年齡等)
(5)

<init>

方法執行,設定對象字段值(按照程式員的意願)。

2. 對象的記憶體布局

  • 對象頭
  • 執行個體資料
  • 對齊填充

    (1) 對象頭

    (a)第一部分用于存儲對象自身的運作時資料,如哈希碼、

    GC

    分代年齡、鎖狀态标志、線程持有的鎖等,成

    為Mark Word

    (b)第二部分是類型指針,對象指向它的類中繼資料的指針。(

    -XX:+UseCompressedOops

    )
32位 64位(未開啟指針壓縮) 64位(開啟指針壓縮)
Mark Word(4 Byte) Mark Word(8 Byte) Mark Word(8 Byte)
Class 指針(4 Byte) Class 指針(8 Byte) Class 指針(4 Byte)

(2) 執行個體資料

執行個體資料是對象真正存儲的有效資訊,是程式中所定義的各種類型字段的内容。這部分内容的 存儲順序受到虛拟機配置設定政策和字段在

Java

中定義的順序有關。

預設配置設定政策是 按照類型寬度由大到小,順序排列,同時相同寬度的放在一起,最後是引用類型。父類中定義的變量放在子類之前。若

-XX:FieldAllocationStyle=0

,則 引用類型放在最前面。
(3) 對齊填充(占位符)

HotSpot VM

自動記憶體管理系統要求對象起始位址是8位元組的整數倍,也就是對象大小必須是8位元組的整數倍。同時,對象頭是8位元組的整數倍(1倍或2倍),如果執行個體資料沒有對齊,則需要通過對其填充來補充。

3. 對象的通路定位

  • 句柄通路
  • 直接指針

(1) 句柄

采用句柄方式,

Java

堆中會有一塊區域作為句柄池,

Java

棧中

reference

存儲的是對象的句柄位址。而句柄中包含了對象執行個體資料與類型資料的各自位址資訊。
Java記憶體區域與記憶體溢出異常OOM——深入了解Java虛拟機Java記憶體區域與OOM記憶體溢出異常
(2) 直接指針

reference

存儲的值直接是對象位址。
Java記憶體區域與記憶體溢出異常OOM——深入了解Java虛拟機Java記憶體區域與OOM記憶體溢出異常

(3) 比較

采用句柄通路的好處是

reference

中存儲的是穩定的句柄位址,在對象移動時隻改變句柄中對象執行個體的位址,

reference

不需要修改。

采用直接指針,速度快,節省了一次指針定位的開銷。

三、

OutOfMemoryError

異常

目标是定位

OOM

異常出現的區域,知道為什麼導緻該異常,以及處理辦法。
(1)

Java

堆溢出
/**
 * VM Args:-Xms20m -Xmx20m(設定堆初始大小、最大大小)     
 * -XX:+HeapDumpOnOutOfMemoryError(設定堆轉儲快照)
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}
           
原因可以分為記憶體洩漏和記憶體溢出,對于記憶體洩漏,可以通過

Eclipse Memory Analzer

分析堆轉儲檔案,分析洩露對象的資訊和GC Roots引用鍊資訊,就可以定位洩露代碼的位置。 如果是記憶體溢出,設定

-Xms -Xmx

.
(2)

Java

虛拟機棧、本地方法棧溢出

棧容量由

-Xss

參數設定。

(3) 方法區和運作時常量池溢出

(1)

JDK1.6

-XX:PermSize -XX:MaxPermSize

限制方法區大小。
/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行為
        List<String> list = new ArrayList<String>();
        // 10MB的PermSize在integer範圍内足夠産生OOM了
        int i = ; 
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
           

: (4) 本機直接記憶體溢出

DirectMemory

容量可通過

-XX:MaxDirectMemorySize

指定,如果不指定,則預設與

Java

堆(

-Xmx

)一樣。