一、JVM 概述
1.1 什么是 JVM?
JVM(Java Virtual Machine,Java 虛擬機)是 Java 程序運行的核心引擎。它像一個“翻譯官”,將 Java 字節碼轉換為機器能理解的指令,并管理程序運行時的內存、線程等資源。
核心功能:
- 跨平臺運行:一次編譯,到處運行(Write Once, Run Anywhere)
- 內存管理:自動分配和回收內存(垃圾回收)
- 安全控制:字節碼驗證、權限管理
類比說明:
- Java 程序?→ 一本用中文寫的菜譜
- JVM?→ 一位精通各國語言的廚師
- 不同操作系統?→ 不同國家的廚房
- 字節碼?→ 國際通用的菜譜符號
二、JVM 核心架構
JVM 由三大核心模塊組成:
模塊 | 功能 | 關鍵技術 |
---|---|---|
類加載系統 | 加載.class文件到內存 | 雙親委派機制 |
運行時數據區 | 管理程序運行時的內存分配 | 堆、棧、方法區 |
執行引擎 | 解釋/編譯字節碼為機器指令 | 解釋器、JIT編譯器、垃圾回收 |
三、類加載機制
3.1 類加載流程
類加載分為三個階段:
- 加載(Loading)
- 查找.class文件
- 將字節碼轉換為方法區的數據結構
- 鏈接(Linking)
- 驗證:檢查字節碼是否符合規范
- 準備:為靜態變量分配內存(默認初始值)
- 解析:將符號引用轉為直接引用
- 初始化(Initialization)
- 執行靜態代碼塊(
<clinit>
方法) - 為靜態變量賦真實值
- 執行靜態代碼塊(
示例:
public class Demo {static int value = 10; // 準備階段 value=0,初始化階段 value=10static {System.out.println("靜態代碼塊執行");}
}
3.2 雙親委派模型
加載器層級:
- Bootstrap ClassLoader:加載
jre/lib
核心庫(如java.lang.*) - Extension ClassLoader:加載
jre/lib/ext
擴展庫 - Application ClassLoader:加載用戶類路徑(classpath)
- 自定義ClassLoader:用戶自定義加載邏輯
工作流程:
- 子加載器收到加載請求后,先委派父加載器處理
- 父加載器無法完成時,子加載器才嘗試加載
優勢:
- 避免核心類被篡改(如自定義java.lang.String)
- 保證類全局唯一性
-
public class ClassLoaderDemo {public static void main(String[] args) {// 查看不同類的加載器System.out.println(String.class.getClassLoader()); // null(Bootstrap加載器)System.out.println(ClassLoaderDemo.class.getClassLoader()); // AppClassLoader} }
四、運行時數據區
4.1 內存結構總覽
-
+-------------------+ | 方法區(Method Area) | ← 存儲類信息、常量、靜態變量 +-------------------+ | 堆(Heap) | ← 所有對象實例和數組 +-------------------+ | 虛擬機棧(VM Stack) | ← 線程私有的方法調用棧幀 | 本地方法棧(Native Stack)| ← 調用本地(Native)方法 | 程序計數器(PC Register) | ← 當前線程執行的字節碼行號 +-------------------+
4.2 堆(Heap)
- 分代設計:
- 新生代(Young Generation):新創建的對象
- Eden區(80%)
- Survivor區(From + To,各10%)
- 老年代(Old Generation):長期存活的對象
- 元空間(Metaspace,JDK8+):類元數據(替代永久代)
- 新生代(Young Generation):新創建的對象
-
對象分配流程:
- 新對象優先分配到Eden區
- Eden滿時觸發Minor GC
- 存活對象復制到Survivor區(年齡+1)
- 年齡達到閾值(默認15)后進入老年代
示例代碼:
public class HeapDemo {public static void main(String[] args) {List<byte[]> list = new ArrayList<>();while (true) {list.add(new byte[1024 * 1024]); // 持續創建1MB數組,觸發OOM}}
}
// 輸出:java.lang.OutOfMemoryError: Java heap space
4.3 虛擬機棧(VM Stack)
- 棧幀結構:
- 局部變量表(Local Variables)
- 操作數棧(Operand Stack)
- 動態鏈接(Dynamic Linking)
- 方法返回地址(Return Address)
- 棧溢出示例:
-
public class StackOverflowDemo {static void recursiveCall() {recursiveCall(); // 無限遞歸}public static void main(String[] args) {recursiveCall();} } // 輸出:java.lang.StackOverflowError
4.4 方法區(Method Area)
- 存儲內容:
- 類結構信息(字段、方法、構造函數)
- 運行時常量池
- JIT編譯后的代碼緩存
-
元空間溢出示例:
-
public class MetaspaceOOM {static class OOMObject {}public static void main(String[] args) {// 使用CGLIB動態生成類Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));while (true) {enhancer.create(); // 持續生成代理類}} } // 輸出:java.lang.OutOfMemoryError: Metaspace
五、垃圾回收(GC)
5.1 判斷對象可回收
- 引用計數法:循環引用問題(已棄用)
- 可達性分析:從GC Roots出發,不可達的對象可回收
- GC Roots包括:
- 虛擬機棧中引用的對象
- 方法區中靜態屬性引用的對象
- 本地方法棧中JNI引用的對象
- GC Roots包括:
- 示例:
-
public class GCRootsDemo {Object instance;public static void main(String[] args) {GCRootsDemo a = new GCRootsDemo();GCRootsDemo b = new GCRootsDemo();a.instance = b;b.instance = a;a = null;b = null; // 此時兩個對象仍互相引用,但不可達,會被回收System.gc();} }
5.2 垃圾回收算法
算法 原理 適用場景 標記-清除 標記可回收對象后清除 老年代(CMS) 復制 將存活對象復制到新空間 新生代(Serial、ParNew) 標記-整理 標記后整理內存空間 老年代(Serial Old) 分代收集 根據對象年齡采用不同算法 現代JVM默認方案
復制算法示意圖:
新生代內存布局:
+-----------+------------+-----------+
| Eden | From(S0) | To(S1) |
+-----------+------------+-----------+
Minor GC后存活對象復制到To區,清空Eden和From
5.3 垃圾收集器
收集器 | 特點 | 適用場景 |
---|---|---|
Serial | 單線程,Stop-The-World | 客戶端模式 |
ParNew | Serial的多線程版本 | 新生代(配合CMS) |
CMS | 并發標記清除,低停頓 | 老年代 |
G1 | 分區收集,可預測停頓時間 | JDK9+默認 |
ZGC | 低延遲(<10ms),大堆內存 | 超大內存應用 |
G1收集器示例配置
java -XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=200 MyApp
六、執行引擎
6.1 解釋執行
- 逐行解釋字節碼:效率低,但啟動快
- 示例:
javap -c MyClass.class
?查看字節碼
6.2 JIT編譯(Just-In-Time)
- 熱點代碼檢測:統計方法調用次數
- 編譯優化技術:
- 方法內聯(Method Inlining)
- 逃逸分析(Escape Analysis)
- 循環展開(Loop Unrolling)
逃逸分析示例:
public class EscapeAnalysisDemo {public static void main(String[] args) {for (int i = 0; i < 1000000; i++) {createObject();}}static void createObject() {Object obj = new Object(); // 對象未逃逸,可能被棧上分配}
}
6.3 分層編譯(Tiered Compilation)
- 編譯級別:
- 0:解釋執行
- 1:簡單快速編譯(C1)
- 2:完全優化編譯(C2)
- 參數控制:
-XX:TieredStopAtLevel=3
七、性能監控與調優
7.1 常用工具
工具 | 功能 | 示例命令 |
---|---|---|
jps | 查看Java進程ID | jps -l |
jstat | 監控GC情況 | jstat -gcutil <pid> 1000 |
jmap | 生成堆轉儲快照 | jmap -dump:format=b,file=heap.bin <pid> |
VisualVM | 圖形化性能分析 | 監控CPU、內存、線程 |
jstat輸出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 25.43 68.50 95.12 91.03 10 0.123 2 0.456 0.579
7.2 常見JVM參數
參數 | 作用 | 示例值 |
---|---|---|
-Xms ?/?-Xmx | 初始/最大堆內存 | -Xms512m -Xmx4g |
-XX:NewRatio | 新生代與老年代比例 | -XX:NewRatio=3 (新生代占1/4) |
-XX:SurvivorRatio | Eden與Surviv區比例 | -XX:SurvivorRatio=8 (Eden:S0:S1=8:1:1) |
-XX:+PrintGCDetails | 打印詳細GC日志 |
八、實戰案例
8.1 內存泄漏排查
步驟:
- 使用
jps
獲取進程ID jmap -dump:format=b,file=heap.bin <pid>
?導出堆快照- 用MAT(Memory Analyzer Tool)分析
- 查找支配樹中的大對象
常見原因:
- 靜態集合類持有對象引用
- 未關閉的數據庫連接
- 監聽器未注銷
8.2 GC優化案例
問題現象:Full GC頻繁,每次耗時2秒
分析步驟:
jstat -gcutil <pid> 1000
?發現老年代快速填滿jmap -histo <pid>
?發現大量相同類實例- 檢查代碼發現緩存未設置上限
解決方案:改用LRU緩存,限制最大條目數
九、JVM發展前沿
9.1 GraalVM
- 多語言支持:Java、JavaScript、Python等
- 原生鏡像(Native Image):提前編譯為本地可執行文件
- 示例:
native-image -jar myapp.jar
9.2 Project Loom
- 虛擬線程(Virtual Threads):輕量級線程,支持百萬級并發
- 示例:
-
Thread.startVirtualThread(() -> {System.out.println("Hello from virtual thread!"); });
9.3 ZGC與Shenandoah
- 亞毫秒級停頓:適用于金融交易系統
- 配置示例
-
java -XX:+UseZGC -Xmx16g -XX:+UseLargePages MyApp