第一章 引言
1.1 JVM 中垃圾收集的簡要概述
JVM(Java Virtual Machine)作為 Java 程序的運行時環境,負責將字節碼加載至內存并執行,同時也承擔著內存管理的重任。垃圾收集(Garbage Collection,簡稱 GC)是 JVM 中的一項核心機制,用于自動釋放不再被使用的內存對象,避免內存泄漏和 OOM(OutOfMemoryError)。
現代 JVM 實現了多種垃圾收集器,每種收集器都有其獨特的設計目標和適用場景,從最基本的單線程收集器到并發、并行、高吞吐、低延遲等收集器,為不同類型的應用提供了靈活的選擇。
1.2 選擇合適垃圾收集器的重要性
在生產環境中,選擇合適的垃圾收集器將直接影響系統性能和響應能力。以下幾個方面尤為關鍵:
吞吐量(Throughput):垃圾收集與應用執行時間的比例。
停頓時間(Pause Time):垃圾收集對應用線程的中斷時間。
并發能力:是否支持多線程并行或并發地進行垃圾回收操作。
選擇錯誤的收集器可能導致性能瓶頸、頻繁 Full GC、應用卡頓,甚至出現頻繁 OOM。
1.3 Serial Old 垃圾收集器簡介
Serial Old 是一種針對老年代的串行垃圾收集器,它是 Serial 收集器的老年代版本,使用單線程的標記-清除-整理(Mark-Compact)算法。盡管它在多核 CPU 的現代環境中并不高效,但由于實現簡單、性能預測性強,它仍在某些特定場景中發揮作用,例如:
JVM 在 Client 模式下運行。
作為 CMS 的后備收集器(CMS 出現 Concurrent Mode Failure 時)。
嵌入式、小內存或單核環境。
本博客將全面解析 Serial Old 的工作機制、應用場景及其與其他收集器的對比,并輔以詳細的配置參數與示例代碼,以幫助讀者在實際工作中做出更精準的 GC 策略選擇。
第二章 JVM 垃圾收集基礎
2.1 什么是垃圾收集?
垃圾收集是自動內存管理的一部分,用于回收不再被任何對象引用的內存資源。在 Java 中,開發者無需手動釋放內存,JVM 會定期通過 GC 機制進行掃描、標記和回收。
示例:對象失去引用
public class GCDemo {public static void main(String[] args) {Object obj = new Object();obj = null; // 原來的對象失去引用,變成垃圾對象System.gc(); // 建議 JVM 執行 GC}
}
2.2 JVM 中的垃圾收集工作原理
JVM 利用多種算法識別哪些對象是"垃圾"。其中,引用計數法因無法處理循環引用而被淘汰,現代 JVM 大多使用可達性分析(Reachability Analysis)。
GC Roots:一組系統定義的起始點(如線程棧、靜態字段、JNI 等)。
可達對象:從 GC Roots 可達的對象被視為存活。
2.3 JVM 內存結構(Java 8)
Java 8 中的堆內存主要分為以下幾部分:
年輕代(Young Generation):存儲新創建的對象,進一步細分為 Eden 和兩個 Survivor 區。
老年代(Old Generation):存放從年輕代晉升的長期存活對象。
永久代(PermGen):存儲類元信息、靜態字段等(Java 8 開始被 MetaSpace 替代,但仍存在于一些文檔描述中)。
圖示:堆內存結構
+------------------------------------------+
| Java Heap |
+-----------------+------------------------+
| Young Gen | Old Gen |
|+-----+---------+| |
||Eden |Survivors|| |
|+-----+---------+| |
+-----------------+------------------------+
不同代的對象使用不同的垃圾收集器進行回收。Serial Old 專注于老年代。
第三章 深入解析 Serial Old 垃圾收集器
3.1 Serial Old 是什么?
Serial Old 垃圾收集器是 Serial 垃圾收集器在老年代的實現版本,其主要特點如下:
串行(Single-threaded):GC 階段只有一個線程工作,不具備并行能力。
老年代專屬:僅用于回收老年代對象。
使用 Mark-Compact(標記-清除-整理)算法:相比于 Mark-Sweep 算法,其在清除后會對對象進行壓縮整理,以減少碎片。
非增量、非并發:GC 期間,所有應用線程(Stop-The-World)都會被暫停,直到回收完成。
雖然 Serial Old 顯得比較原始,但其實現穩定、行為可預測,因此在以下幾種場景仍有應用價值:
Client 模式下的默認老年代收集器;
CMS 的后備方案(當 CMS 失敗時,Fallback 至 Serial Old);
嵌入式、小內存設備或無并發能力的平臺;
用于測試和教學場景,便于調試 GC 行為。
3.2 Serial Old 的適用場景
盡管現代應用多選擇并行或并發 GC,但 Serial Old 仍然適用于以下特定情境:
1. 單核或極低并發設備
如嵌入式設備、路由器、工控設備等,它們本身計算資源受限,無法有效利用并行 GC。
2. 對 GC 可預測性要求高的場景
由于其單線程、邏輯簡單,Serial Old 的 GC 行為可預測、易于調試,是性能調優中的一個理想參考對象。
3. 作為 CMS 的后備收集器
在 CMS 收集失敗(出現 Concurrent Mode Failure)時,JVM 會自動退回 Serial Old 進行一次完整的 Full GC。
4. 教學、實驗或分析場景
由于其可控性高、流程簡單,非常適合作為教學或 GC 日志分析的工具。
3.3 Serial Old 的工作原理
Serial Old 使用的是“標記-清除-整理算法(Mark-Compact)”,其具體流程如下:
1. 標記(Mark)階段
從 GC Roots 出發,通過可達性分析(Graph Traversal)查找所有仍在被引用的對象;
所有被標記的對象被視為“存活”。
2. 清除(Sweep)階段
遍歷整個堆空間,將未被標記的對象視為垃圾,進行清除;
清除后會產生內存碎片。
3. 整理(Compact)階段
將存活對象壓縮到內存的一端,騰出連續的空閑區域,防止碎片;
整理過程中需要更新所有引用指針。
整體流程圖(文本):
[堆初始狀態]
+--A--+--B--+--X--+--Y--+--Z--+
(其中 A/B 為存活對象,X/Y/Z 為垃圾對象)[標記階段]
標記 A、B[清除階段]
回收 X、Y、Z[整理階段]
+--A--+--B--+----------------+
這個過程是“Stop-The-World”的,意味著 GC 期間應用線程必須全部暫停,容易造成長時間停頓,尤其在堆較大時表現更明顯。
3.4 Serial Old 的停頓機制
Serial Old 垃圾收集器采用完全的 Stop-The-World 模式,意味著:
在 GC 開始時,所有應用線程都會被暫停;
GC 線程單線程執行;
GC 完成后,應用線程才會恢復執行。
GC 日志示例
[Full GC (System.gc()) [Tenured: 2048K->512K(10240K), 0.0234560 secs] 4096K->1536K(20480K), [Perm: 2560K->2560K(21248K)], 0.0237890 secs]
說明:
Tenured 表示老年代變化(2048K 回收至 512K);
整體耗時為 0.0237890 秒;
發生的是一次 Full GC,使用的正是 Serial Old 收集器。
3.5 Serial Old 與年輕代收集器的搭配
在 JVM 中,老年代收集器通常與年輕代收集器協同工作。Serial Old 常與 Serial 垃圾收集器(年輕代) 組合,構成完整的串行垃圾收集策略,適用于小型應用。
收集器組合示例:
年輕代收集器 | 老年代收集器 | 使用命令行參數 |
---|---|---|
Serial | Serial Old | -XX:+UseSerialGC |
這組組合適用于:
單核 CPU;
需要最大化可預測性;
對延遲要求不高的應用。
3.6 Serial Old 的實現邏輯(簡要源碼級別概覽)
在 OpenJDK 中,Serial Old 的核心實現類如下:
MarkSweepCompact
:執行標記-清除-整理;CompactibleFreeListSpace
:描述老年代的內存結構;GenMarkSweep
:負責執行老年代的 Serial Old GC。
偽代碼簡化如下:
void do_full_gc() {mark(); // 標記存活對象sweep(); // 清除垃圾對象compact(); // 壓縮堆,清除碎片
}
雖然整體邏輯簡單,但在整理階段涉及地址計算、指針修復,因此仍需謹慎優化。
?
第四章 Serial Old 與其他垃圾收集器的比較
在 Java 8 中,針對老年代的垃圾收集器主要有三種:Serial Old、Parallel Old 和 CMS。它們各自具備不同的設計目標和性能特性。本章將通過結構性對比,幫助開發者理解 Serial Old 與其他老年代垃圾收集器之間的差異,以便在特定場景下作出合適選擇。
4.1 Serial Old vs Parallel Old
4.1.1 并發能力對比
特性 | Serial Old | Parallel Old |
---|---|---|
是否多線程 | 否 | 是 |
回收算法 | 標記-清除-整理 | 標記-清除-整理 |
應用線程停頓 | 是(STW) | 是(STW) |
吞吐量 | 中等 | 高 |
實現復雜度 | 簡單 | 中等 |
適用場景 | 小堆、單核、Client | 大堆、多核、Server |
4.1.2 說明
Parallel Old 是 Parallel Scavenge 年輕代收集器的老年代搭檔,適用于對吞吐量敏感的系統;
Serial Old 則由于其單線程模型,不適合高并發環境,但在資源受限平臺仍有優勢;
兩者都采用 Mark-Compact 算法,但 Parallel Old 使用多線程并行壓縮以縮短 GC 時間。
4.2 Serial Old vs CMS(Concurrent Mark Sweep)
4.2.1 對比表
特性 | Serial Old | CMS(已廢棄) |
是否多線程 | 否 | 是 |
并發能力 | 無 | 支持標記、清除階段并發 |
回收算法 | 標記-清除-整理 | 標記-清除(無整理) |
內存碎片 | 少(有整理) | 多(無整理) |
STW 停頓 | 長 | 較短 |
失敗回退 | - | Serial Old |
適用場景 | 小堆、嵌入式、備用 GC | 中等堆、低延遲、高響應需求 |
4.2.2 說明
CMS(Concurrent Mark Sweep) 強調低停頓,但容易產生內存碎片;
CMS 沒有整理(compact)過程,當出現 Promotion Failed 或 Concurrent Mode Failure 時,JVM 會自動切換回 Serial Old 執行 Full GC;
CMS 在 Java 9 后被廢棄,G1 成為替代方案,但在 Java 8 中仍然是重要的低延遲回收器選擇。
4.3 與 G1 的比較(補充)
盡管 G1 屬于后代收集器,但了解 Serial Old 與 G1 的差異有助于明確過渡路徑。
特性 | Serial Old | G1(Java 9+ 默認) |
分代結構 | 固定(Young/Old) | Region(動態劃分) |
是否多線程 | 否 | 是 |
并發能力 | 否 | 是(并發標記、清理) |
回收算法 | Mark-Compact | Incremental Region-based |
延遲可控 | 否 | 是 |
吞吐量 | 中等 | 中等 |
4.4 如何選擇收集器?
應用類型 | 推薦收集器 | 原因 |
嵌入式 / 單核 | Serial + Serial Old | 簡潔、預測性強 |
高吞吐應用 | Parallel + Parallel Old | 最大化 CPU 利用率 |
響應時間敏感應用 | CMS(或 G1) | 停頓時間短,適合交互式系統 |
大堆 / 高并發 | G1 或 ZGC | 多線程回收、并發處理、低延遲 |
第五章 配置 Serial Old
要在 Java 應用中使用 Serial Old 垃圾收集器,開發者需要通過 JVM 啟動參數進行配置。本章將詳解 Serial Old 的啟用方式、相關 JVM 參數、常見組合方案,并提供適用于不同場景的配置示例。
5.1 如何啟用 Serial Old 垃圾收集器
Serial Old 本身并不能獨立工作,它通常與年輕代的 Serial 收集器共同配置。完整啟用方式如下:
啟用命令
java -XX:+UseSerialGC -Xms256m -Xmx256m -jar yourApp.jar
該命令中:
-XX:+UseSerialGC
:啟用 Serial + Serial Old 收集器組合;-Xms
和-Xmx
設置堆的初始與最大值(建議設置為相同,避免運行時動態擴容)。
一旦啟用 Serial GC,老年代自動采用 Serial Old,不需額外指定。
5.2 核心配置參數說明
以下是與 Serial Old 配置密切相關的 JVM 參數:
參數 | 描述 |
---|---|
-XX:+UseSerialGC | 啟用 Serial 和 Serial Old 組合收集器 |
-XX:NewRatio=2 | 老年代與年輕代大小比例,默認值為 2 |
-XX:SurvivorRatio=8 | Eden 與 Survivor 區大小比例 |
-Xms<size> / -Xmx<size> | 設置堆的初始與最大內存大小 |
-XX:+PrintGCDetails | 打印 GC 詳細日志 |
-XX:+PrintGCDateStamps | 打印 GC 日志時間戳 |
-Xloggc:<file> | 輸出 GC 日志到指定文件 |
示例:完整配置參數
java -Xms512m -Xmx512m \-XX:+UseSerialGC \-XX:NewRatio=2 \-XX:SurvivorRatio=8 \-XX:+PrintGCDetails \-XX:+PrintGCDateStamps \-Xloggc:./gc.log \-jar myApp.jar
5.3 常見使用場景下的配置建議
場景一:嵌入式或資源受限環境
java -Xms128m -Xmx128m -XX:+UseSerialGC -jar app.jar
適用于內存資源極小的系統(如 ARM 單板機);
配置簡單、穩定,避免多線程 GC 帶來的調度開銷。
場景二:教學/調試用途(查看 GC 行為)
java -Xms256m -Xmx256m \-XX:+UseSerialGC \-XX:+PrintGCDetails \-XX:+PrintGCDateStamps \-Xloggc:gc.log \-jar app-debug.jar
打印詳細 GC 日志便于分析 GC 階段和時間消耗;
常用于 GC 教程或性能測試。
場景三:CMS 回退配置(無需手動指定)
若應用使用 CMS(
-XX:+UseConcMarkSweepGC
),當 CMS 發生 Concurrent Mode Failure,JVM 自動使用 Serial Old 做 Full GC;
可通過
PrintGCDetails
觀察 GC 類型判斷是否已回退。
5.4 配置建議總結
應用類型 | 建議參數 | 補充說明 |
嵌入式 / 小型應用 | -XX:+UseSerialGC | 簡單、可預測性高 |
調試 / 教學用途 | +UseSerialGC +PrintGCDetails | 方便觀察 GC 日志 |
CMS 回退處理 | 默認包含 Serial Old | 無需顯式啟用,CMS 失敗時自動切換 |
第六章 Serial Old 性能調優
雖然 Serial Old 垃圾收集器結構簡單,但合理的參數調優依然可以幫助開發者減少 GC 頻率、縮短停頓時間、提高回收效率。本章將從堆內存配置、晉升策略、GC 日志分析、對象生命周期控制等方面,系統講解如何優化 Serial Old 的運行性能。
6.1 調優目標與原則
使用 Serial Old 時,調優目標主要聚焦在以下幾點:
控制 Full GC 的頻率和持續時間;
減少對象在老年代的駐留;
避免因堆空間不足引發頻繁 GC 或 OOM;
使 GC 行為更加可預測。
調優時遵循以下原則:
預分配足夠內存,減少動態擴容;
減少老年代晉升對象的比例;
通過日志掌握 GC 節奏和壓力點。
6.2 堆內存參數調整
合理設置初始堆和最大堆,有助于降低 GC 次數和頻繁的內存擴容帶來的額外成本。
參數建議:
-Xms512m -Xmx512m # 初始堆和最大堆設置為一致,防止動態調整
-XX:NewRatio=2 # 年輕代 : 老年代 = 1 : 2
-XX:SurvivorRatio=8 # Eden : Survivor = 8 : 1 : 1
配置解讀:
年輕代大,意味著更多對象可以在年輕代被清除,減少晉升到老年代的頻率;
Survivor 空間適當調大,避免對象提前晉升。
6.3 控制對象晉升到老年代的節奏
在 Serial Old 的使用中,老年代 GC(即 Full GC)是系統停頓的主要來源之一,因此減少對象進入老年代尤為重要。
晉升機制相關參數:
參數 | 描述 |
---|---|
-XX:MaxTenuringThreshold=15 | 對象在 Survivor 區經歷幾次 GC 后晉升老年代 |
-XX:+PrintTenuringDistribution | 打印對象年齡分布 |
示例:
-XX:MaxTenuringThreshold=10
-XX:+PrintTenuringDistribution
設置更高的晉升閾值,有助于讓短生命周期對象在年輕代被回收;
通過日志分析,找出哪些對象在晉升前存活較久,從而識別內存熱點。
6.4 使用 GC 日志分析 GC 過程
打印 GC 日志是進行調優的基礎。通過觀察 GC 頻率、耗時、堆使用率等指標,可以判斷是否需要擴容、調整參數或優化代碼。
日志啟用示例:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc_serial.log
關鍵指標解析:
[Full GC] 出現頻率:頻繁出現說明老年代壓力過大;
Tenured: 使用比率:老年代使用接近上限時,GC 會變頻繁;
GC 耗時:關注 STW 停頓時間,通常在毫秒到秒級不等;
示例日志片段:
2025-07-13T14:22:01.789+0800: 10.123: [Full GC (System.gc())[Tenured: 10240K->512K(10240K), 0.1234567 secs] 20480K->1536K(20480K), [Perm: 2560K->2560K(21248K)], 0.1237890 secs]
可見老年代幾乎被占滿,引發一次 Full GC;
GC 效果較好,但 STW 達到了 123ms。
6.5 分析 GC 熱點對象
利用工具進一步分析老年代對象存活情況,有助于識別內存泄漏風險與“長命對象”。
可用工具:
JVisualVM:圖形界面觀察堆中熱點類與 GC 行為;
MAT(Memory Analyzer Tool):分析 heap dump,找出大對象與 GC roots 路徑;
JFR(Java Flight Recorder):可跟蹤對象生命周期和 GC 事件。
6.6 避免手動調用 System.gc()
默認情況下,調用 System.gc()
會觸發一次 Full GC,使用 Serial Old 作為收集器時,會產生較長 STW 停頓,應避免在業務邏輯中顯式調用。
關閉自動調用選項:
-XX:+DisableExplicitGC
禁用顯式 Full GC 調用,有助于防止代碼中不必要的 GC 停頓。
6.7 調優策略總結
調優策略 | 目的 |
增大年輕代比例 | 減少晉升至老年代的對象數量 |
延長對象晉升周期 | 提高 Survivor 區利用率 |
啟用 GC 日志 | 定位頻繁 GC、觀察堆壓力點 |
禁用 System.gc() | 避免不必要的 Full GC |
使用分析工具定位泄漏問題 | 優化內存結構和對象生命周期管理 |
?
?
?
?
?
?
?