🗑? JVM垃圾回收機制深度解析
文章目錄
- 🗑? JVM垃圾回收機制深度解析
- 🔍 垃圾判定算法
- 🔢 引用計數法
- 🌐 可達性分析算法
- 🔄 垃圾回收算法
- 🏷? 標記-清除算法
- 📋 復制算法
- 🔧 標記-整理算法
- 🏗? 分代收集算法
- 🛠? 常見垃圾收集器
- 🔄 Serial 收集器
- ? ParNew 收集器
- 🚀 Parallel 收集器
- 🔧 CMS 收集器
- 🌟 G1 收集器
- ? 垃圾回收調優
- 🔧 常用 JVM 調優參數
- 🛠? 調優工具使用:JConsole、VisualVM
- JConsole
- VisualVM
- 🔬 實戰案例分析
- 案例一:內存泄漏排查
- 案例二:GC停頓時間過長
- 案例三:頻繁的Young GC
- 📊 總結與展望
- 垃圾回收技術的發展趨勢
- 最佳實踐總結
🔍 垃圾判定算法
在JVM中,垃圾回收的第一步是確定哪些對象是"垃圾"。JVM主要采用兩種算法來判斷對象是否可以被回收。
🔢 引用計數法
引用計數法是一種直觀的垃圾判定方法,其核心思想是:為每個對象添加一個引用計數器,當有引用指向該對象時計數器加1,引用失效時計數器減1,計數器為0時即可回收。
public class ReferenceCountingExample {public Object instance = null;public static void main(String[] args) {ReferenceCountingExample objA = new ReferenceCountingExample();ReferenceCountingExample objB = new ReferenceCountingExample();// 對象之間相互引用objA.instance = objB;objB.instance = objA;// 將objA和objB置為null,斷開外部引用objA = null;objB = null;// 此時objA和objB指向的對象雖然已經不可能再被訪問,// 但由于它們相互引用,引用計數都不為0,導致無法被回收System.gc(); // 觸發垃圾回收}
}
引用計數法的優缺點:
優點 | 缺點 |
---|---|
實現簡單,判定效率高 | 無法解決循環引用問題 |
對象可以很快被回收 | 計數器增減操作帶來額外開銷 |
內存管理的實時性較高 | 需要額外的空間存儲計數器 |
💡 注意:雖然引用計數法簡單直觀,但由于無法解決循環引用問題,現代JVM(如HotSpot)并不使用此算法作為主要的垃圾判定方法。
🌐 可達性分析算法
可達性分析算法是現代JVM采用的主要垃圾判定算法,其核心思想是:通過一系列稱為"GC Roots"的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路徑稱為"引用鏈"。如果某個對象到GC Roots之間沒有任何引用鏈相連,則證明此對象是不可能再被使用的,可以被回收。
在Java中,可作為GC Roots的對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
- 活躍線程
public class GCRootsExample {// 靜態屬性引用的對象作為GC Rootsprivate static Object staticObject;// 常量引用的對象作為GC Rootsprivate static final Object CONST_OBJECT = new Object();public void method() {// 虛擬機棧中引用的對象作為GC RootsObject localObject = new Object();// 使用本地方法nativeMethod();}private native void nativeMethod(); // 本地方法棧中引用的對象作為GC Rootspublic static void main(String[] args) {// main方法是一個活躍線程,其中引用的對象作為GC RootsObject mainObject = new Object();// 創建一個不可達對象Object unreachableObject = new Object();unreachableObject = null; // 斷開引用,此對象變為不可達System.gc(); // 觸發垃圾回收}
}
可達性分析算法的優缺點:
優點 | 缺點 |
---|---|
能解決循環引用問題 | 需要STW(Stop-The-World),暫停所有用戶線程 |
判定更加精確 | 實現復雜 |
被大多數現代JVM采用 | 可能造成較長時間的停頓 |
💡 注意:可達性分析算法執行時必須保證整個分析過程的一致性,因此需要STW。JVM后續的優化方向之一就是如何減少STW的時間。
🔄 垃圾回收算法
確定了哪些對象需要回收后,接下來就是如何高效地回收這些對象。JVM主要采用以下幾種垃圾回收算法。
🏷? 標記-清除算法
標記-清除(Mark-Sweep)算法是最基礎的垃圾回收算法,分為兩個階段:
- 標記階段:標記出所有需要回收的對象
- 清除階段:統一回收所有被標記的對象
graph LRsubgraph 標記前A1[對象A] --- B1[對象B] --- C1[對象C] --- D1[對象D] --- E1[對象E]endsubgraph 標記后A2[對象A] --- B2[對象B 標記] --- C2[對象C] --- D2[對象D 標記] --- E2[對象E 標記]endsubgraph 清除后A3[對象A] --- C3[對象C] --- 空1((空閑)) --- 空2((空閑)) --- 空3((空閑))end標記前 --> 標記后 --> 清除后classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;classDef marked fill:#f9d4d4,stroke:#333,stroke-width:1px;classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;class A1,B1,C1,D1,E1,A2,C2,A3,C3 normal;class B2,D2,E2 marked;class 空1,空2,空3 empty;
標記-清除算法的優缺點:
優點 | 缺點 |
---|---|
實現簡單 | 效率不高,標記和清除兩個過程效率都不高 |
是其他算法的基礎 | 產生大量內存碎片,導致無法分配大對象 |
📋 復制算法
復制(Copying)算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。
graph LRsubgraph 復制前subgraph From空間A1[對象A] --- B1[對象B] --- C1[對象C] --- D1[對象D] --- E1[對象E]endsubgraph To空間空1((空閑)) --- 空2((空閑)) --- 空3((空閑)) --- 空4((空閑)) --- 空5((空閑))endendsubgraph 復制后subgraph To空間變為From空間A2[對象A] --- C2[對象C] --- 空6((空閑)) --- 空7((空閑)) --- 空8((空閑))endsubgraph From空間變為To空間空9((空閑)) --- 空10((空閑)) --- 空11((空閑)) --- 空12((空閑)) --- 空13((空閑))endend復制前 --> 復制后 classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;class A1,B1,C1,D1,E1,A2,C2 normal;class 空1,空2,空3,空4,空5,空6,空7,空8,空9,空10,空11,空12,空13 empty;
復制算法的優缺點:
優點 | 缺點 |
---|---|
實現簡單,運行高效 | 內存利用率低,只有一半內存可用 |
沒有內存碎片 | 對象存活率高時,復制操作開銷大 |
適合新生代回收 | 需要額外空間做分配擔保 |
💡 實際應用:HotSpot VM的新生代使用了復制算法的變種 - Eden和Survivor區的比例是8:1:1,每次只有10%的內存會被"浪費"。
🔧 標記-整理算法
標記-整理(Mark-Compact)算法是針對老年代對象存活率高的特點設計的。標記過程與標記-清除算法一樣,但后續步驟不是直接清理,而是讓所有存活的對象都向內存空間一端移動,然后清理掉邊界以外的內存。
graph LRsubgraph 標記前A1[對象A] --- B1[對象B] --- C1[對象C] --- D1[對象D] --- E1[對象E]endsubgraph 標記后A2[對象A] --- B2[對象B 標記] --- C2[對象C] --- D2[對象D 標記] --- E2[對象E 標記]endsubgraph 整理后A3[對象A] --- C3[對象C] --- 空1((空閑)) --- 空2((空閑)) --- 空3((空閑))end標記前 --> 標記后 --> 整理后classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;classDef marked fill:#f9d4d4,stroke:#333,stroke-width:1px;classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;class A1,B1,C1,D1,E1,A2,C2,A3,C3 normal;class B2,D2,E2 marked;class 空1,空2,空3 empty;
標記-整理算法的優缺點:
優點 | 缺點 |
---|---|
不會產生內存碎片 | 移動對象需要更新引用,效率較低 |
內存利用率高 | 需要STW,停頓時間可能較長 |
適合老年代回收 | 實現復雜 |
🏗? 分代收集算法
分代收集算法并不是一種具體的垃圾回收算法,而是根據對象的生命周期特征,將內存劃分為幾個區域,并在不同區域采用不同的收集算法。
graph TDsubgraph JVM堆內存subgraph 新生代E[Eden區] --- S1[Survivor 1區]E --- S2[Survivor 2區]endsubgraph 老年代O[Old區]endend新生代 -.-> |對象晉升| 老年代classDef eden fill:#f9d4d4,stroke:#333,stroke-width:1px;classDef survivor fill:#d4f9d4,stroke:#333,stroke-width:1px;classDef old fill:#d4d4f9,stroke:#333,stroke-width:1px;class E eden;class S1,S2 survivor;class O old;
分代收集的策略:
- 新生代:大多數對象朝生夕滅,存活率低,采用復制算法
- 老年代:對象存活率高,采用標記-清除或標記-整理算法
分代收集的對象晉升過程:
public class GenerationalGCExample {public static void main(String[] args) {// 1. 新對象優先在Eden區分配byte[] allocation1 = new byte[30900*1024];// 2. Eden區滿,觸發Minor GC,存活對象復制到Survivor區byte[] allocation2 = new byte[900*1024];// 3. 多次GC后,對象年齡達到閾值,晉升到老年代for (int i = 0; i < 15; i++) {byte[] allocation3 = new byte[1000*1024];allocation3 = null; // 使對象變為垃圾System.gc(); // 建議JVM進行垃圾回收}// 4. 大對象直接進入老年代byte[] allocation4 = new byte[10*1024*1024];}
}
分代收集算法的優缺點:
優點 | 缺點 |
---|---|
針對不同代的特點采用最合適的算法 | 實現復雜 |
提高了垃圾回收效率 | 需要維護多個內存區域 |
減少了內存碎片和停頓時間 | 各代之間的對象引用需要特殊處理 |
🛠? 常見垃圾收集器
JVM提供了多種垃圾收集器,每種收集器都有其特點和適用場景。
🔄 Serial 收集器
Serial收集器是最基本、歷史最悠久的垃圾收集器,它是一個單線程收集器,在進行垃圾收集時,必須暫停所有用戶線程。
Serial收集器的特點:
- 單線程收集
- 簡單高效,對于單CPU環境來說是首選
- 收集過程中需要STW,停頓時間較長
- 新生代采用復制算法,老年代采用標記-整理算法
- 適用于客戶端應用,如桌面應用程序
啟用參數: -XX:+UseSerialGC
? ParNew 收集器
ParNew收集器是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為和Serial收集器完全一樣。
ParNew收集器的特點:
- 多線程收集,充分利用多核CPU優勢
- 收集過程中需要STW,但停頓時間比Serial短
- 新生代采用復制算法
- 是許多服務端應用首選的新生代收集器
- 可與CMS收集器配合使用
啟用參數: -XX:+UseParNewGC
🚀 Parallel 收集器
Parallel收集器(也稱為Parallel Scavenge收集器)是一個新生代收集器,使用復制算法,也是并行的多線程收集器。
public class ParallelGCExample {public static void main(String[] args) {// 設置Parallel收集器參數// -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:GCTimeRatio=19// 創建大量對象觸發GCList<byte[]> list = new ArrayList<>();for (int i = 0; i < 1000; i++) {byte[] bytes = new byte[1024 * 1024]; // 1MBlist.add(bytes);if (i % 10 == 0) {list.clear(); // 釋放引用,觸發GC}}}
}
Parallel收集器的特點:
- 多線程收集,注重吞吐量
- 可設置最大垃圾收集停頓時間和吞吐量
- 自適應調節策略,動態調整參數
- 新生代采用復制算法,老年代采用標記-整理算法
- 適用于后臺運算而不需要太多交互的應用
啟用參數:
-XX:+UseParallelGC
:新生代使用Parallel Scavenge,老年代使用Serial Old-XX:+UseParallelOldGC
:新生代使用Parallel Scavenge,老年代使用Parallel Old
🔧 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它非常適合互聯網站或者B/S系統的服務端,這類應用通常重視服務的響應速度,希望系統停頓時間最短。
CMS收集器的工作流程:
- 初始標記:標記GC Roots能直接關聯到的對象,速度很快,需要STW
- 并發標記:進行GC Roots Tracing,與用戶線程并發執行
- 重新標記:修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,需要STW
- 并發清除:清除標記為垃圾的對象,與用戶線程并發執行
CMS收集器的特點:
- 并發收集,低停頓
- 采用標記-清除算法,會產生內存碎片
- 對CPU資源敏感
- 無法處理浮動垃圾(并發清除階段產生的垃圾)
- 需要預留一部分內存作為并發收集時的預留空間
啟用參數: -XX:+UseConcMarkSweepGC
🌟 G1 收集器
G1(Garbage-First)收集器是一款面向服務端應用的垃圾收集器,它是JDK 9的默認垃圾收集器。G1收集器的設計目標是取代CMS收集器,它同樣具有并發和并行、低停頓的特點,同時兼顧了高吞吐量。
G1收集器的工作原理:
- 將整個Java堆劃分為多個大小相等的獨立區域(Region)
- 保留分代概念,但不再是物理隔離
- 建立可預測的停頓時間模型
- 采用"標記-整理"算法,降低內存碎片
- 采用"復制"算法,提高回收效率
public class G1GCExample {public static void main(String[] args) {// 設置G1收集器參數// -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=2m// 創建大量對象觸發GCList<byte[]> list = new ArrayList<>();Random random = new Random();while (true) {int size = random.nextInt(1024 * 1024); // 0-1MBbyte[] bytes = new byte[size];list.add(bytes);if (list.size() > 10000) {list.subList(0, 5000).clear(); // 釋放一部分引用System.gc(); // 建議JVM進行垃圾回收}try {Thread.sleep(1); // 控制速度} catch (InterruptedException e) {e.printStackTrace();}}}
}
G1收集器的特點:
- 并發與并行收集
- 分代收集
- 空間整合(標記-整理+復制算法,無內存碎片)
- 可預測的停頓時間模型
- 適用于大內存、多核CPU的服務器環境
啟用參數: -XX:+UseG1GC
? 垃圾回收調優
垃圾回收調優是JVM性能優化的重要部分,合理的GC參數配置可以顯著提升應用性能。
🔧 常用 JVM 調優參數
參數 | 說明 | 示例值 |
---|---|---|
-Xms | 初始堆大小 | -Xms4g |
-Xmx | 最大堆大小 | -Xmx4g |
-Xmn | 新生代大小 | -Xmn1g |
-XX:SurvivorRatio | Eden區與Survivor區的比例 | -XX:SurvivorRatio=8 |
-XX:MaxTenuringThreshold | 對象晉升老年代的年齡閾值 | -XX:MaxTenuringThreshold=15 |
-XX:ParallelGCThreads | 并行GC線程數 | -XX:ParallelGCThreads=4 |
-XX:ConcGCThreads | 并發GC線程數 | -XX:ConcGCThreads=2 |
-XX:InitiatingHeapOccupancyPercent | 觸發并發GC的堆占用率閾值 | -XX:InitiatingHeapOccupancyPercent=45 |
-XX:MaxGCPauseMillis | 最大GC停頓時間 | -XX:MaxGCPauseMillis=200 |
-XX:G1HeapRegionSize | G1收集器的Region大小 | -XX:G1HeapRegionSize=4m |
不同場景的JVM參數配置示例:
- 高吞吐量場景(后臺批處理):
java -Xms4g -Xmx4g -Xmn1g -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:+UseNUMA -jar app.jar
- 低延遲場景(交互式應用):
java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45 -jar app.jar
- 內存受限場景(嵌入式或容器環境):
java -Xms256m -Xmx512m -XX:+UseSerialGC -jar app.jar
🛠? 調優工具使用:JConsole、VisualVM
JConsole
JConsole是JDK自帶的圖形化監控工具,可以監控本地和遠程的Java應用程序。
使用步驟:
- 啟動JConsole:在命令行中輸入
jconsole
- 選擇要監控的Java進程
- 查看內存、線程、類、VM摘要等信息
JConsole監控內存的關鍵指標:
- 堆內存使用情況
- 非堆內存使用情況
- 各代內存使用情況
- GC次數和時間
VisualVM
VisualVM是一個功能更強大的監控和分析工具,它集成了多種JDK命令行工具的功能。
使用步驟:
- 啟動VisualVM:在命令行中輸入
jvisualvm
- 選擇要監控的Java進程
- 查看概述、監視、線程、抽樣器、分析器等信息
VisualVM的主要功能:
- 監控CPU、堆內存、類、線程
- 執行堆轉儲和線程轉儲
- 分析性能熱點
- 安裝插件擴展功能
public class GCTuningExample {public static void main(String[] args) {// 啟動參數: -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStampsSystem.out.println("GC調優示例啟動,進程ID: " + ManagementFactory.getRuntimeMXBean().getName());System.out.println("使用JConsole或VisualVM連接此進程進行監控");List<byte[]> list = new ArrayList<>();int count = 0;try {while (true) {// 創建1MB的對象byte[] bytes = new byte[1024 * 1024];list.add(bytes);count++;if (count % 10 == 0) {// 每創建10個對象,清理一半對象int half = list.size() / 2;list.subList(0, half).clear();System.out.println("已創建對象數: " + count + ", 當前列表大小: " + list.size());}Thread.sleep(100); // 控制速度}} catch (InterruptedException e) {e.printStackTrace();}}
}
🔬 實戰案例分析
案例一:內存泄漏排查
問題描述:一個Web應用在運行一段時間后,內存占用持續增加,最終導致OutOfMemoryError。
排查步驟:
- 收集證據:使用
-XX:+HeapDumpOnOutOfMemoryError
參數獲取堆轉儲文件 - 分析堆轉儲:使用MAT(Memory Analyzer Tool)分析堆轉儲文件
- 定位問題:發現大量的Session對象未被釋放
- 解決方案:修復Session管理代碼,確保不再使用的Session被及時釋放
public class MemoryLeakExample {// 問題代碼:使用靜態集合存儲對象,導致內存泄漏private static final Map<String, Object> cache = new HashMap<>();public void addToCache(String key, Object value) {cache.put(key, value); // 對象被添加后永遠不會被移除}// 修復后的代碼:使用WeakHashMap或添加過期機制private static final Map<String, Object> fixedCache = new WeakHashMap<>();public void addToFixedCache(String key, Object value) {fixedCache.put(key, value); // 當key不再被引用時,對應的entry會被自動移除}
}
案例二:GC停頓時間過長
問題描述:一個交易系統在高峰期出現間歇性響應緩慢,監控發現是GC停頓時間過長導致的。
排查步驟:
- 收集GC日志:使用
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
參數 - 分析GC日志:使用GCViewer等工具分析GC日志
- 定位問題:發現Full GC頻繁發生,停頓時間長
- 解決方案:從Serial Old收集器切換到G1收集器,并調整參數
優化前的JVM參數:
-Xms2g -Xmx2g -XX:+UseParallelGC
優化后的JVM參數:
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45
案例三:頻繁的Young GC
問題描述:一個數據處理應用頻繁出現Young GC,影響整體吞吐量。
排查步驟:
- 監控GC活動:使用VisualVM觀察GC頻率和內存使用情況
- 分析對象分配:使用
-XX:+PrintTenuringDistribution
參數查看對象年齡分布 - 定位問題:發現大量短生命周期的小對象被頻繁創建
- 解決方案:優化代碼,減少對象創建,增加對象復用
public class FrequentYoungGCExample {// 問題代碼:在循環中頻繁創建對象public void processData(List<String> data) {for (String item : data) {// 每次迭代都創建新的StringBuilderStringBuilder builder = new StringBuilder();builder.append("Processing: ").append(item);System.out.println(builder.toString());}}// 優化后的代碼:復用StringBuilder對象public void processDataOptimized(List<String> data) {StringBuilder builder = new StringBuilder();for (String item : data) {builder.setLength(0); // 清空StringBuilderbuilder.append("Processing: ").append(item);System.out.println(builder.toString());}}
}
📊 總結與展望
垃圾回收技術的發展趨勢
-
低延遲垃圾收集器:如ZGC(Z Garbage Collector)和Shenandoah,它們的目標是將GC停頓時間控制在10ms以內,無論堆的大小如何
-
并發收集的改進:減少或消除STW時間,提高并發收集效率
-
自適應調優:更智能的GC參數自動調整,減少人工干預
-
大內存優化:針對TB級別內存的優化,支持超大堆
-
非易失性內存支持:利用新型存儲技術,如Intel的Optane持久內存
最佳實踐總結
-
選擇合適的垃圾收集器:
- 吞吐量優先:Parallel收集器
- 響應時間優先:CMS或G1收集器
- 大內存低延遲:ZGC或Shenandoah收集器
-
合理設置內存大小:
- 避免設置過大的堆內存,增加GC壓力
- 避免設置過小的堆內存,導致頻繁GC
- 新生代與老年代的比例通常為1:2或1:3
-
優化對象生命周期:
- 減少臨時對象的創建
- 使用對象池復用對象
- 注意集合類的使用,避免內存泄漏
-
監控與分析:
- 定期檢查GC日志和內存使用情況
- 使用專業工具分析性能瓶頸
- 建立性能基準,及時發現異常
如果這篇博客對你有幫助,不要忘記點贊、收藏和分享哦!