JVM筆記-運行時內存區域劃分

1. 概述

Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分爲若干個不同的數據區域。它們各有用途,有些隨着虛擬機進程的啓動一直存在(堆、方法區),有些則隨着用戶線程的啓動和結束而建立和銷燬(程序計數器、虛擬機棧、本地方法棧)。

《Java 虛擬機規範》中規定 Java 虛擬機管理的內存包括以下幾個區域:

下面簡要分析各個區域的特點。

2. JVM 運行時內存區域

2.1 程序計數器

程序計數器(Program Counter Register),可以看做當前線程所執行的字節碼的行號指示器(其實就是記錄代碼執行到了哪裏)。特點如下:

  • 線程私有;

  • 佔用內存空間較小;

  • 若線程執行的是 Java 方法,記錄的是虛擬機字節碼指令地址;若執行的是本地(Native)方法,則爲空(Undefined);

  • 該區域是唯一一個在《Java 虛擬機規範》中規定無任何 OutOfMemoryError 的區域。

主要作用:記錄線程執行到了哪裏。

2.2 Java 虛擬機棧

Java 虛擬機棧(Java Virtual Machine Stacks):Java 方法執行的線程內存模型。

每個方法被執行時,虛擬機棧都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態連接、方法出口等信息。每個方法從被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。其中局部變量表包括:

  • Java 虛擬機基本數據類型(8 種)

  • 對象引用(reference 類型,可能是一個指向對象起始地址的指針)

  • returnAddress

這些數據類型在局部變量表中的存儲空間以局部變量槽(Slot)表示,其中 long 和 double 佔用兩個槽,其他類型佔用一個槽。局部變量表所需內存空間在編譯期完成分配,當進入一個方法時,該方法需要在棧幀中分配多大的局部變量空間是完全確定的,運行期間不會改變其大小。

虛擬機棧的特點:

  • 線程私有;

  • 生命週期與線程相同;

  • 兩類異常

    • 線程請求的棧深度大於虛擬機所允許的深度時拋出 StackOverflowError 異常;

    • 棧擴展時無法申請到足夠的內存時拋出 OutOfMemoryError 異常。

主要目的:Java 方法執行的線程內存模型。

2.3 本地方法棧

本地方法棧(Native Method Stacks)與 Java 虛擬機棧作用類似。二者區別:

  • Java 虛擬機棧爲 JVM 執行 Java 方法(字節碼)服務;

  • 本地方法棧爲 JVM 使用到的本地(Native)方法服務。

異常與 Java 虛擬機棧相同。

主要目的:Native 方法執行的線程內存模型。

2.4 Java 堆

對多數應用來說,Java 堆(Java Heap)是 JVM 管理的內存中最大的一塊。

唯一目的:存放對象實例(【幾乎所有】的對象實例都在這裏分配內存)。

《Java 虛擬機規範》描述:所有對象實例及數組都應在堆上分配。

而從實現角度看,由於即使編譯技術(尤其是逃逸分析技術的日漸強大),"棧上分配"等手段使得對象並非完全在堆上分配。

特點:

  • 線程共享

  • 虛擬機啓動時創建

PS: "新生代"、"老年代"、"Eden 區"等一系列對堆的區域劃分,只是部分垃圾收集器的一些共性或設計風格,而非虛擬機的固有內存佈局,更非《Java 虛擬機規範》的劃分。

將 Java 堆細分的目的只是爲了更好地回收內存,或者更快地分配內存。

2.5 方法區

方法區(Method Area):用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據,該區域也是線程共享的。又稱"非堆"。

與方法區聯繫密切的一個概念是"永久代",下面簡要介紹。

永久代

"永久代(Permanent Generation)",可以理解爲 JDK 1.8 之前 HotSpot 虛擬機對《Java 虛擬機規範》中"方法區"的實現。從 JDK 1.6、1.7 到 1.8+,HotSpot 虛擬機的運行時數據區變遷示意圖如下:

HotSpot VM JDK 1.6 的運行時數據區示意圖如下:

JDK 1.7 中,將 1.6 中永久代的字符串常量池和靜態變量等移到了堆中,如下(虛線框表示已移除):

而到了 JDK 1.8,則完全廢棄了"永久代",改用了在本地內存中實現的"元空間(Metaspace)",將 JDK 1.7 中永久代剩餘的部分(主要是類型信息)移到了元空間,如下(虛線框表示已移除):

從上面幾張圖可以看出永久代和元空間的主要區別有以下兩點:

  1. 存儲位置不同

    1. 永久代是 JVM 內存的一部分,元空間在本地內存中(JVM 內存之外);

    2. 永久代使用不當可能導致 OOM,元空間一般不會。

  2. 存儲內容不同:元空間存儲的是「類型信息」(即類的元信息),而永久代除了類型信息,還包括「字符串常量池」和「靜態變量」等(可以理解爲元空間是永久代拆分出來的一部分)。

那麼問題來了:爲什麼要把永久代替換爲元空間呢?

原因大概有以下幾點:

  1. Oracle 收購了兩種 JVM:HotSpot VM 和 JRockit VM,並且想要將它們整合,但二者方法區實現差異較大;

  2. 字符串存在永久代中,容易出現性能問題和 OOM;

  3. 類及方法的信息大小較難確定,永久代大小難以確定:太小易導致永久代溢出,太大則易導致老年代溢出(JVM 內存是有限的,此消彼長);

  4. 永久代會爲垃圾回收帶來不必要的複雜度,且回收效率較低("性價比"低)。

2.6 運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。

Class 文件中除了有類的版本、字段、方法、接口等描述外信息,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

相比於 Class 文件常量池的一個重要特性是「動態性」,運行期間也可以將新的常量放入池中(例如 String 類的 intern() 方法)。

可能產生的異常:OutOfMemoryError。

2.7 直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也非《Java 虛擬機規範》定義的內存區域。但該部分內存被頻繁使用(例如 NIO),而且可能導致 OutOfMemoryError。

3. OOM異常實踐

3.0 操作系統及 JDK 版本

  • 操作系統:macOS Mojave 10.14.5

  • JDK 1.8

$ java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
  • JDK 1.7

$ java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

3.1 Java 堆溢出

  • 示例代碼(JDK 1.8)

public class HeapOOM {
  public static void main(String[] args) {
    List<Object> list = new ArrayList<>();
    while (true) {
      list.add(new OOMObject());
    }
  }


  static class OOMObject {
  }
}
  • VM 參數

# 設置堆空間大小爲 20M
-Xms20m -Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
  • 異常信息

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid39807.hprof ...
Heap dump file created [27773554 bytes in 0.342 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:3210)
  ...

3.2 虛擬機棧和本地方法棧溢出

  • 示例代碼(JDK 1.8)

public class StackOverflowError {
  private int stackLength = 1;


  private void stackLeak() {
    stackLength++;
    stackLeak();
  }


  public static void main(String[] args) {
    JvmStackOverflow sof = new JvmStackOverflow();
    try {
      sof.stackLeak();
    } catch (Throwable ex) {
      // 注意這裏是 Throwable,而非 Exception (Error 不是 Exception)
      System.out.println("stack length: " + sof.stackLength);
      throw ex;
    }
  }
}
  • VM 參數

由於 HotSpot 虛擬機不區分 Java 虛擬機棧和本地方法棧。因此 -Xoss 參數(設置本地方法棧大小)並沒有作用,棧空間只能由 -Xss 參數。

# Java 虛擬機棧大小
-Xss160K
  • 異常信息

stack length: 772
Exception in thread "main" java.lang.StackOverflowError
  at com.jaxer.example.JvmStackOverflow.stackLeak(JvmStackOverflow.java:11)
  at com.jaxer.example.JvmStackOverflow.stackLeak(JvmStackOverflow.java:12)
  ...

3.3 方法區和運行時常量池溢出

3.3.1 字符串常量
  • 示例代碼

public class RuntimeConstantPoolOOM {
  static String baseStr = "string";


  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    while (true) {
      String s = baseStr + baseStr;
      baseStr = s;
      list.add(s.intern());
    }
  }
}

JDK 1.8 參數及異常:

  • VM 參數

# 最大堆空間爲 10M,永久代爲 10M (爲便於觀察,打印了啓動命令和 GC 信息)
-Xmx10m -XX:PermSize=10m -XX:MaxPermSize=10m 
-XX:+PrintGCDetails -XX:+PrintCommandLineFlags
  • 異常信息

Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:3332)
  ...

JDK 1.7 參數及異常信息:

  • VM 參數

# 設置永久代大小爲 10M
-XX:PermSize=10m -XX:MaxPermSize=10m
  • 異常信息

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:2367)
  ...

參考鏈接:https://www.cnblogs.com/paddix/p/5309550.html

3.3.2 類型信息
  • 示例代碼

package com.jaxer.example.cglib;


public class OOMObject {
}

使用 CGLib 生成代碼:

public class PermGenOOM {
    public static void main(String[] args) {
        try {
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invoke(o, objects);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

JDK 1.8 參數及異常:

  • VM 參數

# 設置元空間大小爲 10M
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
  • 異常信息

java.lang.OutOfMemoryError: Metaspace
  at java.lang.Class.forName0(Native Method)
  at java.lang.Class.forName(Class.java:348)
  ...

JDK 1.7 參數及異常信息:

  • VM 參數

# 設置永久代大小爲 10M
-XX:PermSize=10m -XX:MaxPermSize=10m -XX:+PrintGCDetails
  • 異常信息

Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

此處的異常無法被捕獲,Debug 模式斷點如下:

可以看到,這裏實際還是永久代(PermGen space)OOM 異常。

3.4 本機直接內存溢出

  • 示例代碼(JDK 1.8)

public class DirectMemoryOOM {
  private static final int _1M = 2014 * 1024;


  public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
    while (true) {
      ByteBuffer buffer = ByteBuffer.allocateDirect(_1M); // java.lang.OutOfMemoryError: Direct buffer memory
//      ByteBuffer buffer = ByteBuffer.allocate(_1M); // java.lang.OutOfMemoryError: Java heap space
      list.add(buffer);
    }
  }
}
  • VM 參數

# 設置堆內存最大爲 20M,直接內存最大爲 10M
-Xmx20m -XX:MaxDirectMemorySize=10m
  • 異常

java.lang.OutOfMemoryError: Direct buffer memory

4. 小結

本文主要分析了《Java 虛擬機規範》中規定的 Java 虛擬機管理的運行時內存區域,並以 HotSpot 虛擬機爲例,分析了 JDK 1.7 和 1.8 內存溢出的情況。主要內容總結如下圖:

PS: 一些虛擬機參數

# 設置堆空間大小
-Xms20m -Xmx20m


# 設置虛擬機棧空間大小
-Xss160K


# 設置永久代大小
-XX:PermSize=10m -XX:MaxPermSize=10m


# 設置元空間大小
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m


# 打印 GC 日誌
-XX:+PrintGCDetails


# 打印命令行參數
-XX:+PrintCommandLineFlags


# 堆棧信息
-XX:+HeapDumpOnOutOfMemoryError


發佈了114 篇原創文章 · 獲贊 40 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章