JVM筆記-運行時內存區域劃分
- 2020 年 3 月 16 日
- 筆記
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 異常。
- 線程請求的棧深度大於虛擬機所允許的深度時拋出 StackOverflowError 異常;
主要目的: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 中永久代剩餘的部分(主要是類型信息)移到了元空間,如下(虛線框表示已移除):
從上面幾張圖可以看出永久代和元空間的主要區別有以下兩點:
-
存儲位置不同
-
- 永久代是 JVM 內存的一部分,元空間在本地內存中(JVM 內存之外);
- 永久代使用不當可能導致 OOM,元空間一般不會。
-
存儲內容不同:元空間存儲的是「類型信息」(即類的元信息),而永久代除了類型信息,還包括「字符串常量池」和「靜態變量」等(可以理解為元空間是永久代拆分出來的一部分)。
那麼問題來了:為什麼要把永久代替換為元空間呢?
原因大概有以下幾點:
- Oracle 收購了兩種 JVM:HotSpot VM 和 JRockit VM,並且想要將它們整合,但二者方法區實現差異較大;
- 字符串存在永久代中,容易出現性能問題和 OOM;
- 類及方法的信息大小較難確定,永久代大小難以確定:太小易導致永久代溢出,太大則易導致老年代溢出(JVM 內存是有限的,此消彼長);
- 永久代會為垃圾回收帶來不必要的複雜度,且回收效率較低("性價比"低)。
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) ...
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