深入理解 JVM 記憶體模型是 Java 效能調校的基礎。本章將詳細介紹 JVM 的記憶體區域劃分、物件記憶體佈局,以及各區域可能發生的記憶體溢位問題。
執行時資料區域概覽#
graph TB
subgraph JVM["JVM 執行時資料區域"]
subgraph Shared["執行緒共享區域"]
subgraph Heap["堆積區 (Heap)"]
Young["新生代<br/>Young"]
Old["老年代<br/>Old"]
end
subgraph Method["方法區 (Method Area)"]
Meta["類別資訊、常數池<br/>靜態變數、JIT 程式碼<br/><i>(Java 8+ = Metaspace)</i>"]
end
end
subgraph Private["執行緒私有區域"]
Stack["VM 堆疊<br/>(VM Stack)<br/>堆疊幀、區域變數<br/>運算元堆疊"]
Native["本地方法堆疊<br/>(Native Stack)<br/>JNI 方法"]
PC["程式計數器<br/>(PC Register)<br/>目前執行指令位址"]
end
end
style Shared fill:#e3f2fd
style Private fill:#fff3e0
style Heap fill:#c8e6c9
style Method fill:#ffccbc堆積區(Heap)#
堆積區是 JVM 管理的最大記憶體區域,用於存放物件實體。
堆積區結構#
graph LR
subgraph Heap["堆積區 (Heap)"]
subgraph Young["新生代 (Young) - 1/3"]
Eden["Eden<br/>(8)"]
S0["S0<br/>(1)"]
S1["S1<br/>(1)"]
end
subgraph Old["老年代 (Old) - 2/3"]
LongLived["長期存活的物件"]
end
end
style Young fill:#c8e6c9
style Old fill:#fff9c4
style Eden fill:#a5d6a7預設比例:
- 新生代 : 老年代 = 1 : 2 (-XX:NewRatio=2)
- Eden : S0 : S1 = 8 : 1 : 1 (-XX:SurvivorRatio=8)
大部分物件都在 Eden 區分配。當 Eden 區滿時觸發 Minor GC,存活的物件會被複製到 Survivor 區。經過多次 GC 仍存活的物件(預設 15 次)會晉升到老年代。
物件分配策略#
// 1. 優先在 Eden 區分配
Object obj = new Object(); // 小物件,分配在 Eden
// 2. 大物件直接進入老年代
// -XX:PretenureSizeThreshold=1048576 (1MB)
byte[] bigArray = new byte[2 * 1024 * 1024]; // 2MB,直接到老年代
// 3. 長期存活的物件進入老年代
// 物件在 Survivor 區每熬過一次 Minor GC,年齡 +1
// 達到 MaxTenuringThreshold(預設 15)後晉升老年代
// 4. 動態物件年齡判定
// 如果 Survivor 區中相同年齡物件大小總和 > Survivor 空間的一半
// 則年齡 >= 該年齡的物件直接進入老年代TLAB(Thread Local Allocation Buffer)#
graph LR
subgraph Eden["Eden 區"]
T1["TLAB-1<br/>Thread-1"]
T2["TLAB-2<br/>Thread-2"]
T3["TLAB-3<br/>Thread-3"]
Shared["共用空間"]
end
style T1 fill:#bbdefb
style T2 fill:#c8e6c9
style T3 fill:#fff9c4
style Shared fill:#f5f5f5TLAB 是每個執行緒在 Eden 區的私有分配區域,可以避免多執行緒分配記憶體時的競爭,提升分配效率。預設開啟(
-XX:+UseTLAB)。
方法區(Method Area)#
方法區存放類別的元資訊,在 Java 8 之後由 Metaspace 實現。
Java 7 vs Java 8+ 的變化#
graph TB
subgraph Java7["Java 7 (永久代 PermGen)"]
subgraph JVM7["JVM 堆積記憶體"]
H7["堆積區<br/>(Heap)"]
P7["永久代 (PermGen)<br/>類別資訊<br/>常數池<br/>靜態變數"]
end
end
subgraph Java8["Java 8+ (元空間 Metaspace)"]
subgraph JVM8["JVM 堆積記憶體"]
H8["堆積區 (Heap)<br/>字串常數池<br/>靜態變數"]
end
subgraph Native["本地記憶體"]
M8["Metaspace<br/>類別資訊"]
end
end
style Java7 fill:#ffccbc
style Java8 fill:#c8e6c9
style P7 fill:#ffcdd2
style M8 fill:#bbdefbJava 8 將永久代移除,改用 Metaspace。主要變化:
- Metaspace 使用本地記憶體,預設不限制大小
- 字串常數池和靜態變數移到堆積區
- 減少了 PermGen 記憶體溢位的問題
Metaspace 參數組態#
# 設定 Metaspace 初始大小
-XX:MetaspaceSize=256m
# 設定 Metaspace 最大大小
-XX:MaxMetaspaceSize=512m
# 類別中繼資料的壓縮空間大小
-XX:CompressedClassSpaceSize=256m虛擬機器堆疊(VM Stack)#
每個執行緒都有自己的虛擬機器堆疊,用於存放堆疊幀。
堆疊幀結構#
flowchart TB
subgraph VMStack["虛擬機器堆疊 (VM Stack)"]
subgraph Frame1["堆疊幀 (Stack Frame)"]
direction TB
LV["區域變數表<br/>(Local Variables)<br/>slot 0: this<br/>slot 1~n: 參數/變數"]
OS["運算元堆疊<br/>(Operand Stack)<br/>執行運算的臨時儲存"]
DL["動態連結<br/>(Dynamic Linking)<br/>指向常數池方法參照"]
RA["方法回傳位址<br/>(Return Address)<br/>退出後的回傳位置"]
end
Frame2["上一個堆疊幀"]
Frame3["..."]
end
LV --> OS --> DL --> RA
Frame1 --> Frame2 --> Frame3
style VMStack fill:#fff3e0
style Frame1 fill:#e3f2fd
style LV fill:#c8e6c9
style OS fill:#fff9c4
style DL fill:#ffccbc
style RA fill:#e1bee7區域變數表的 Slot 複用#
public void example() {
{
int a = 100; // slot 1
System.out.println(a);
}
// a 的作用域結束,slot 1 可以被複用
int b = 200; // 複用 slot 1
System.out.println(b);
}區域變數表大小計算
| 資料型別 | 佔用 Slot 數 |
|---|---|
boolean, byte, char, short, int, float | 1 |
long, double | 2 |
reference | 1 |
returnAddress | 1 |
實體方法的 slot 0 固定存放 this 參照。
物件記憶體佈局#
物件頭(Object Header)#
flowchart TB
subgraph Layout["物件記憶體佈局"]
direction TB
subgraph Header["物件頭 (Object Header)"]
direction TB
MW["Mark Word (標記字)<br/>64 位元:HashCode、GC 年齡、鎖狀態"]
CP["類型指標 (Class Pointer)<br/>指向方法區中的類別元資料<br/>(壓縮指標:32 位元)"]
AL["陣列長度 (Array Length)<br/>只有陣列物件才有,4 位元組"]
end
ID["實體資料 (Instance Data)<br/>物件的欄位值,按分配策略排列"]
PD["對齊填充 (Padding)<br/>確保物件大小是 8 位元組的倍數"]
end
MW --> CP --> AL
Header --> ID --> PD
style Layout fill:#fff3e0
style Header fill:#e3f2fd
style MW fill:#ffcdd2
style CP fill:#c8e6c9
style AL fill:#fff9c4
style ID fill:#bbdefb
style PD fill:#f5f5f5壓縮指標(Compressed Oops)#
# 預設開啟(堆積 < 32GB 時)
-XX:+UseCompressedOops
# 壓縮類型指標(Java 8+)
-XX:+UseCompressedClassPointers壓縮指標將 64 位元指標壓縮為 32 位元,可以顯著減少記憶體佔用。當堆積超過 32GB 時會自動關閉。建議堆積大小設定在 32GB 以內以享受壓縮指標的好處。
使用 JOL 分析物件佈局#
// 添加依賴
// <dependency>
// <groupId>org.openjdk.jol</groupId>
// <artifactId>jol-core</artifactId>
// <version>0.16</version>
// </dependency>
import org.openjdk.jol.info.ClassLayout;
public class ObjectLayoutDemo {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}輸出範例(開啟壓縮指標):
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000001
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total字段重排列#
class FieldReorder {
byte b; // 1 byte
long l; // 8 bytes
int i; // 4 bytes
char c; // 2 bytes
}
// JVM 會重排列欄位以減少 padding:
// 實際佈局可能是:
// long l (8) -> int i (4) -> char c (2) -> byte b (1) -> padding (1)
// 而非聲明順序記憶體溢位場景分析#
1. 堆積區溢位(OutOfMemoryError: Java heap space)#
// 模擬堆積區溢位
// java -Xmx20m HeapOOM
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
}
}
}解決方案:
# 1. 增加堆積大小
-Xmx4g -Xms4g
# 2. 檢查記憶體洩漏
# 使用 jmap 產生堆積傾印
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 使用 MAT 或 VisualVM 分析2. Metaspace 溢位(OutOfMemoryError: Metaspace)#
// 模擬 Metaspace 溢位
// java -XX:MaxMetaspaceSize=20m MetaspaceOOM
public class MetaspaceOOM {
public static void main(String[] args) {
int i = 0;
while (true) {
// 使用 CGLib 動態產生類別
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOOM.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) ->
proxy.invokeSuper(obj, args1));
enhancer.create();
System.out.println("已建立 " + (++i) + " 個類別");
}
}
}解決方案:
# 1. 增加 Metaspace 大小
-XX:MaxMetaspaceSize=512m
# 2. 檢查是否有大量動態類別產生
# 框架如 CGLib、ASM、Groovy 等可能產生大量類別
# 3. 監控 Metaspace 使用情況
jstat -gc <pid> 10003. 堆疊溢位(StackOverflowError)#
// 模擬堆疊溢位
public class StackOOM {
private int stackDepth = 0;
public void recursiveCall() {
stackDepth++;
recursiveCall();
}
public static void main(String[] args) {
StackOOM oom = new StackOOM();
try {
oom.recursiveCall();
} catch (StackOverflowError e) {
System.out.println("堆疊深度:" + oom.stackDepth);
}
}
}解決方案:
# 1. 增加堆疊大小
-Xss2m
# 2. 檢查遞迴呼叫是否有終止條件
# 3. 考慮將遞迴改為迭代4. 直接記憶體溢位(OutOfMemoryError: Direct buffer memory)#
// 模擬直接記憶體溢位
// java -XX:MaxDirectMemorySize=20m DirectMemoryOOM
public class DirectMemoryOOM {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
while (true) {
// 分配直接記憶體
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
list.add(buffer);
}
}
}解決方案:
# 1. 增加直接記憶體大小
-XX:MaxDirectMemorySize=256m
# 2. 手動呼叫 System.gc() 觸發直接記憶體回收
# 3. 使用 Cleaner 或反射呼叫 DirectByteBuffer.cleaner().clean()直接記憶體的 GC 問題
直接記憶體不在 JVM 堆積中,Full GC 時才會觸發其回收。如果直接記憶體用完但堆積還充足,可能不會觸發 GC,導致 OOM。
解決方法:
// 手動清理直接記憶體
public static void cleanDirectBuffer(ByteBuffer buffer) {
if (buffer.isDirect()) {
try {
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
cleaner.clean();
} catch (Exception e) {
// 處理例外
}
}
}記憶體參數組態建議#
常用記憶體參數#
# 堆積設定
-Xms4g # 初始堆積大小
-Xmx4g # 最大堆積大小
-Xmn2g # 新生代大小
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
# Metaspace 設定
-XX:MetaspaceSize=256m # 初始 Metaspace
-XX:MaxMetaspaceSize=512m # 最大 Metaspace
# 堆疊設定
-Xss1m # 執行緒堆疊大小
# 直接記憶體
-XX:MaxDirectMemorySize=256m不同場景的組態建議#
| 場景 | 堆積組態 | 新生代組態 | 說明 |
|---|---|---|---|
| Web 應用 | 4-8GB | 1/3 堆積 | 物件生命週期短 |
| 批次處理 | 視資料量 | 較小 | 大量長生命週期物件 |
| 微服務 | 1-2GB | 預設 | 容器環境注意限制 |
| 大資料處理 | 16GB+ | 較小 | 大量快取物件 |
建議將
-Xms和-Xmx設為相同值,避免堆積動態調整帶來的效能開銷。在容器環境中,注意使用-XX:+UseContainerSupport(Java 10+)讓 JVM 正確識別容器記憶體限制。
總結#
JVM 記憶體模型的核心要點:
- 執行緒共享:堆積區存放物件,方法區(Metaspace)存放類別資訊
- 執行緒私有:VM 堆疊、本地方法堆疊、程式計數器
- 物件佈局:物件頭 + 實體資料 + 對齊填充
- 壓縮指標:堆積 < 32GB 時有效,顯著減少記憶體佔用
- OOM 排查:根據錯誤類型定位問題區域,使用對應工具分析