Java 開發者不用自己管記憶體釋放,靠的就是 JVM 內建的 Garbage Collection。但「不用自己管」不代表「不用理解」,GC 的行為直接影響應用程式的吞吐量和延遲,而且這幾乎是 Java 面試的必考題。這篇從 GC 的基本原理講起,整理 JVM Heap 的記憶體結構、三種基礎回收演算法,再逐一比較目前 JDK 內建的七種收集器,適合正在深入研究 JVM 或準備技術面試的開發者。
GC 在解決什麼問題
C 或 C++ 的開發者需要手動呼叫 free() 或 delete 來釋放記憶體,忘了釋放就是 memory leak,釋放錯了就是 dangling pointer。Java 的做法是把記憶體管理交給 JVM:程式只負責建立物件,JVM 在背景自動找出不再被使用的物件並回收它們佔用的記憶體,這個機制就是 Garbage Collection。
GC 帶來的代價是 CPU 資源的消耗。回收器需要花時間掃描物件、標記存活的參照、搬移資料、釋放空間,這些工作或多或少會影響應用程式的執行。不同的 GC 實作就是在「回收效率」「停頓時間」「記憶體利用率」之間做不同的取捨。
JVM Heap 的記憶體結構
理解 GC 之前,得先知道 JVM 的 Heap 怎麼劃分。大多數收集器採用分代設計(Generational),背後的核心假設是:絕大多數物件存活時間很短,只有少數會長期留在記憶體裡。這個現象在學術上叫做 Weak Generational Hypothesis,而實際統計也確實如此,典型 Java 應用裡超過九成的物件在第一次 GC 就會被回收。
不過並非所有收集器都採用這種模式,ZGC 和 Shenandoah 原本不分代(ZGC 在 JDK 21 才加入分代版本),Epsilon 則完全不回收,後面介紹各收集器時會詳細說明。
基於這個假設,JVM 把 Heap 分成幾個區域:
- Young Generation(年輕代):新建立的物件都先放在這裡。Young Gen 再細分為一個 Eden 區和兩個 Survivor 區(S0、S1),HotSpot 預設比例是 Eden : S0 : S1 = 8 : 1 : 1(由
-XX:SurvivorRatio=8控制)。物件在 Eden 建立,經過一次 Minor GC 存活後搬到 Survivor,在 Survivor 之間來回搬移幾次後如果還活著,就晉升到 Old Gen。 - Old Generation(老年代):存活時間較長的物件最終會被搬到這裡。Old Gen 的 GC(Major GC 或 Full GC)頻率低但耗時較長,因為需要掃描的物件數量多、存活比例高。
- Metaspace:存放類別的 metadata(class 結構、方法資訊、常量池等),從 Java 8 開始取代了 PermGen。舊的 PermGen 有固定大小上限,在動態載入大量 class 的場景(如 Spring、Hibernate)容易爆
OutOfMemoryError: PermGen space。Metaspace 改用 native memory(OS 記憶體)而不是 Heap,預設不設上限,這個問題基本消失,但仍可以用-XX:MaxMetaspaceSize手動限制。

分代設計讓 GC 可以把大部分精力花在 Young Gen,因為那裡的物件數量多但存活率低,回收起來又快又有效率。Old Gen 因為存活率高、物件大,回收成本高,所以盡量降低觸發頻率。
怎麼判斷物件可以被回收
GC 的第一步是判斷哪些物件還「活著」,哪些已經「死了」。有兩種經典做法:
Reference Counting(參照計數)
每個物件維護一個計數器,有新的參照指向它就加 1,參照消失就減 1,計數歸零就代表沒人在用、可以回收。概念簡單,Python 的 CPython 實作就是用這個方式。但這個方法有個致命問題:循環參照。如果 A 參照 B、B 又參照 A,即使外面沒有任何人在用 A 和 B,兩個計數器都不會歸零,記憶體就永遠不會被回收。
Reachability Analysis(可達性分析)
JVM 採用的是這個方式。從一組稱為 GC Roots 的起點開始,沿著物件之間的參照鏈往下走,能走到的物件就是存活的,走不到的就是垃圾。GC Roots 包含:
- 各個執行緒的 stack frame 中的區域變數
- 靜態變數(
static欄位) - JNI 參照
- 被 synchronized 鎖住的物件
可達性分析不怕循環參照,因為判斷的依據是「從 GC Roots 能不能走到」,而不是「有沒有人參照我」。A 跟 B 互相參照但從 GC Roots 走不到的話,兩個都會被判定為垃圾。
四種引用類型與回收優先順序
可達性分析判斷的是「能不能走到」,但 Java 還區分了「用什麼方式走到」。從 JDK 1.2 開始,Java 把物件的參照分成四種類型,GC 對它們的回收策略不同:
- Strong Reference(強引用):最常見的參照方式,
Object obj = new Object()就是強引用。只要強引用還在,GC 絕對不會回收這個物件,即使記憶體不夠也只會丟OutOfMemoryError。 - Soft Reference(軟引用):用
SoftReference<T>包裝。物件只被軟引用指向時,GC 在記憶體充足的情況下不會回收它,但在記憶體不足、即將 OOM 之前會把它回收掉。適合用來實作記憶體敏感的快取:有空間就留著加速存取,空間不夠就自動釋放。 - Weak Reference(弱引用):用
WeakReference<T>包裝。比軟引用更弱,不管記憶體夠不夠,只要 GC 執行就會回收只被弱引用指向的物件。WeakHashMap就是用弱引用實作的,當 key 物件沒有其他強引用時,對應的 entry 會自動被清除。 - Phantom Reference(虛引用):用
PhantomReference<T>包裝,必須搭配ReferenceQueue使用。虛引用不影響物件的生命週期,透過它也拿不到物件(get()永遠回傳null)。它的用途是在物件被 GC 回收後收到通知,用來做資源清理,例如管理 direct memory 或 native 資源的釋放。
回收優先順序是:虛引用 > 弱引用 > 軟引用 > 強引用。換個角度理解:強引用是「一定不回收」,軟引用是「記憶體不夠才回收」,弱引用是「GC 跑到就回收」,虛引用是「隨時回收,只是通知我一聲」。面試時常見的延伸問題是「怎麼用 Soft Reference 實作快取」和「WeakHashMap 的 key 什麼時候會消失」。
三種基礎回收演算法
判斷出哪些物件是垃圾之後,接下來是怎麼回收。所有 GC 收集器的底層都是這三種演算法的組合或變體:
Mark-Sweep(標記清除)
分兩階段:先從 GC Roots 走一遍標記所有存活物件(Mark),再掃描整個區域把沒被標記的物件清掉(Sweep)。優點是不需要搬移物件,缺點是清除後記憶體會產生大量碎片,後續要分配大物件時可能找不到連續的空間。
Copying(複製)
把記憶體分成兩等份,每次只用其中一半。GC 的時候把存活的物件複製到另一半,然後把原來那半整個清空。沒有碎片問題,而且分配新物件時只需要移動指標,速度很快。代價是可用記憶體只有一半。Young Gen 的 Eden + 兩個 Survivor 就是 Copying 的變體,因為 Young Gen 的存活率低(通常不到一成),Survivor 不需要跟 Eden 一樣大,實際記憶體浪費沒有理論上那麼嚴重。HotSpot 預設的 Eden : S0 : S1 比例是 8:1:1,只浪費了 10% 的 Young Gen 空間。
Mark-Compact(標記整理)
先標記存活物件(跟 Mark-Sweep 一樣),然後把所有存活物件往記憶體的一端壓縮,最後清掉邊界外的空間。既沒有碎片,也不需要犧牲一半記憶體。但壓縮的過程需要搬移物件、更新所有指向它們的參照,成本比 Mark-Sweep 高。Old Gen 因為存活率高,用 Copying 會搬太多東西,通常採用 Mark-Compact 或 Mark-Sweep。
七種 GC 收集器比較
JDK 歷代累積了多種收集器,各自針對不同場景最佳化,以下按出現順序逐一介紹。
Serial GC
最早期的收集器,從頭到尾只用一條 GC 執行緒處理回收工作。GC 執行的時候,所有應用程式的執行緒都必須暫停,等 GC 做完才能繼續跑,這個暫停就是常聽到的 Stop-The-World(STW)。Young Gen 用前面介紹的 Copying 演算法(把存活物件複製到 Survivor,清空 Eden),Old Gen 用 Mark-Compact(標記存活物件後壓縮到一端)。
因為只有一條 GC 執行緒,沒有多執行緒協調的開銷,在小型應用或嵌入式環境(記憶體小、CPU 少)反而表現不錯。JVM 參數是 -XX:+UseSerialGC。現在幾乎不會在伺服器端使用,但理解它的行為有助於對比後面的收集器。
Parallel GC(Throughput Collector)
Serial GC 的多執行緒版本,用多條 GC 執行緒同時處理,大幅縮短 STW 時間。Young Gen 用平行的 Copying,Old Gen 用平行的 Mark-Compact。
設計目標是最大化吞吐量(應用程式實際執行時間佔總時間的比例),所以又叫 Throughput Collector。適合批次處理、離線運算這種不怕偶爾停頓幾百毫秒但在意整體處理速度的場景。JVM 參數是 -XX:+UseParallelGC,在 JDK 8 是預設收集器。
CMS(Concurrent Mark Sweep)
從 JDK 1.4.2 開始提供,設計目標是降低 Old Gen 的停頓時間。CMS 的回收過程大部分跟應用程式並行執行,只有初始標記(Initial Mark)和重新標記(Remark)兩個短暫階段需要 STW。Old Gen 用 Mark-Sweep(不壓縮),Young Gen 仍用 ParNew(平行 Copying)。
CMS 的問題在於 Mark-Sweep 不壓縮記憶體,跑久了碎片會越來越嚴重。碎片嚴重到分配不出連續空間時,CMS 會退化成一次 Full GC(Serial Old),停頓時間反而比 Parallel GC 更長。另外 CMS 在並行標記階段會跟應用程式搶 CPU,吞吐量因此下降。
CMS 在 JDK 9 被標記為 deprecated,JDK 14 正式移除。現在不建議在新專案使用,但面試還是經常問到它的設計思路和缺陷,因為 G1 正是為了解決 CMS 的問題而設計的。
G1(Garbage-First)
從 JDK 7u4 開始提供,JDK 9 起成為預設收集器。G1 打破了傳統的連續分代佈局,把整個 Heap 切成大量固定大小的 Region(預設 2048 個,每個 1–32 MB),每個 Region 動態標記為 Eden、Survivor、Old 或 Humongous(存放超過半個 Region 大小的大物件)。
G1 的核心策略是「優先回收垃圾最多的 Region」(這就是 Garbage-First 名稱的由來),用最少的時間回收最多的空間。它可以設定一個目標停頓時間 -XX:MaxGCPauseMillis(預設 200ms),G1 會盡量在這個時間預算內完成回收。
回收過程分幾個階段:並行標記(Concurrent Marking)找出各 Region 的存活率,接著根據停頓時間目標選出一批 Region 做 Mixed GC(同時回收 Young + 部分 Old Region)。Region 之間用 Copying,所以不會產生碎片。
G1 在大多數伺服器端場景是穩定的選擇,在吞吐量和停頓時間之間取得了不錯的平衡。不過如果對停頓時間的要求是個位數毫秒等級,G1 可能還是不夠,就需要往下看 ZGC 或 Shenandoah。
ZGC
Oracle 從 JDK 11 開始開發的低延遲收集器,JDK 15 正式 production-ready,JDK 21 推出了分代版本(Generational ZGC)。設計目標是把 GC 停頓控制在次毫秒等級(sub-millisecond),不隨 Heap 大小而增長。
ZGC 的關鍵技術是 Colored Pointers 和 Load Barriers。它在物件指標裡嵌入額外的 metadata bits 來記錄物件的搬移狀態,應用程式讀取物件參照時會觸發 Load Barrier,如果發現物件已經被搬走就自動修正指標。這讓 ZGC 可以在幾乎不停頓的情況下並行搬移物件,連 Mark-Compact 的壓縮都能做到幾乎不暫停。
JDK 21 的 Generational ZGC 加入了分代概念,把 Young 和 Old 物件分開處理,對短命物件的回收效率比非分代版本好很多。JDK 25 已經將 Generational ZGC 設為預設模式,非分代版本標記為 deprecated。JVM 參數是 -XX:+UseZGC。
ZGC 適合對延遲極度敏感的場景:即時交易系統、互動式應用、需要超大 Heap(TB 等級)的服務。代價是吞吐量通常比 G1 或 Parallel GC 低幾個百分點,因為 Load Barrier 的額外開銷分散在整個應用程式的執行過程中。
Shenandoah
Red Hat 主導開發的低延遲收集器,JDK 12 開始納入 OpenJDK(注意:Oracle 的 JDK 發行版沒有包含 Shenandoah)。設計目標和 ZGC 類似,都是把停頓時間控制在極低的範圍,不受 Heap 大小影響。
跟 ZGC 不同的是,Shenandoah 不用 Colored Pointers,而是用 Brooks Pointer(每個物件多一個間接指標)配合 Load 和 Store Barriers 來實現並行搬移。兩者的停頓表現在實務上非常接近,差異更多在實作細節和生態系支援。
JVM 參數是 -XX:+UseShenandoahGC。如果團隊使用的是 Red Hat 相關的 JDK 發行版(如 Red Hat build of OpenJDK),Shenandoah 會是比較自然的選擇。
Epsilon(No-Op GC)
JDK 11 引入的實驗性收集器,它的行為是:只分配記憶體,完全不回收。Heap 滿了就直接 OutOfMemoryError。
Epsilon 不是拿來跑 production 的,它的用途是效能測試和基準測量。想知道 GC 對應用程式的延遲和吞吐量影響多大?用 Epsilon 跑一次就能得到「完全沒有 GC 開銷」的基準線,再跟其他收集器的結果比較。另外也適合生命週期極短的程式(跑完就結束,JVM 退出時整塊 Heap 直接歸還 OS),或者用來驗證某段程式碼到底會不會產生垃圾物件。JVM 參數是 -XX:+UseEpsilonGC。
收集器對照表
| 收集器 | Young Gen 演算法 | Old Gen 演算法 | 停頓時間 | 適用場景 | JDK 狀態 |
|---|---|---|---|---|---|
| Serial | Copying | Mark-Compact | 長(單執行緒 STW) | 小型應用、嵌入式 | 仍可用 |
| Parallel | Copying(平行) | Mark-Compact(平行) | 中(多執行緒 STW) | 批次處理、高吞吐量 | 仍可用,JDK 8 預設 |
| CMS | ParNew(Copying) | Concurrent Mark-Sweep | 短(並行回收) | 低延遲 Web 服務 | JDK 14 移除 |
| G1 | Region-based Copying + 並行標記 | 可控(預設 200ms) | 通用伺服器端 | JDK 9+ 預設 | |
| ZGC | Colored Pointers + 並行搬移 | 次毫秒 | 超低延遲、大 Heap | JDK 15 正式,JDK 21 分代版 | |
| Shenandoah | Brooks Pointer + 並行搬移 | 次毫秒 | 低延遲(Red Hat 生態) | JDK 12+(非 Oracle JDK) | |
| Epsilon | 不回收 | 無停頓 | 效能基準測試 | JDK 11+ 實驗性 | |
怎麼選收集器
選收集器沒有標準答案,取決於應用程式的特性和優先考量。簡單來說就是停頓時間和記憶體開銷之間的取捨:追求低延遲的收集器(ZGC、Shenandoah)需要更多記憶體開銷來維持並行運作,追求高吞吐量的收集器(Parallel、Serial)記憶體開銷低但停頓時間長。

以下幾個判斷方向可以參考:
- 不確定選什麼:用 G1。JDK 9 以後它是預設值,在大多數場景表現均衡,不需要太多調校就能有不錯的效果。
- 追求最大吞吐量(批次處理、離線運算、MapReduce):Parallel GC。它把所有 CPU 資源集中在回收上,整體處理速度通常比其他收集器更快,代價是偶爾會有較長的停頓。
- 延遲敏感(即時交易、互動式 API、需要穩定的 P99 回應時間):ZGC 或 Shenandoah。兩者的停頓都能控制在個位數毫秒以下,差異在生態系和 JDK 發行版支援。
- Heap 超大(數十 GB 到 TB 等級):ZGC。它的停頓時間不隨 Heap 大小增長,G1 在超大 Heap 上的 Mixed GC 可能超過目標停頓時間。
- 記憶體和 CPU 資源極有限(容器限制 256MB、單核):Serial GC。沒有多執行緒協調的額外開銷,在資源受限的環境反而是更輕量的選擇。
實務上更重要的不是選到「理論最佳」的收集器,而是在真實流量下做效能測試。同一個應用程式換不同收集器跑 benchmark,觀察吞吐量、P99 延遲、GC 停頓頻率和時間,用數據決定比用規則決定可靠得多。
常用 JVM GC 參數速查
| 參數 | 說明 |
|---|---|
-XX:+UseSerialGC | 使用 Serial GC |
-XX:+UseParallelGC | 使用 Parallel GC |
-XX:+UseG1GC | 使用 G1 GC(JDK 9+ 預設) |
-XX:+UseZGC | 使用 ZGC |
-XX:+UseShenandoahGC | 使用 Shenandoah GC |
-XX:+UseEpsilonGC | 使用 Epsilon GC |
-Xms / -Xmx | Heap 初始大小 / 最大大小 |
-Xmn | Young Gen 大小 |
-XX:MaxGCPauseMillis=N | G1 / ZGC 目標停頓時間(毫秒) |
-XX:ParallelGCThreads=N | STW 階段使用的 GC 執行緒數 |
-XX:ConcGCThreads=N | 並行 GC 執行緒數 |
-XX:NewRatio=N | Old Gen : Young Gen 的比例(預設 2,即 Old 佔 2/3) |
-XX:SurvivorRatio=N | Eden : Survivor 的比例(預設 8) |
-verbose:gc | 輸出 GC 日誌(JDK 9+ 用 -Xlog:gc) |
-Xlog:gc* | 輸出詳細 GC 日誌(JDK 9+) |
面試常見問題整理
以下整理幾個在 Java 面試中高頻出現的 GC 相關問題,重點不在背答案,而是理解背後的設計邏輯。
Minor GC、Major GC、Full GC 的差別
Minor GC 只回收 Young Gen,觸發頻率高但速度快。Major GC 回收 Old Gen,不同收集器的定義略有差異。Full GC 回收整個 Heap(Young + Old + Metaspace),通常代表記憶體壓力很大。需要注意的是「Major GC」這個詞在不同文獻中定義不一致,有些文件把 Old Gen GC 叫 Major GC,有些把 Full GC 叫 Major GC,面試時如果被問到,把這個歧義點講出來反而是加分。
為什麼 Young Gen 用 Copying 而不是 Mark-Compact
因為 Young Gen 的存活率極低。Copying 的成本跟存活物件數量成正比,存活越少搬得越少。Mark-Compact 的成本跟整個區域大小相關,因為要掃描整個空間再壓縮。Young Gen 九成以上物件第一次 GC 就死了,用 Copying 只要搬剩下不到一成的存活物件,效率高很多。
G1 和 CMS 的設計差異
CMS 的 Old Gen 是一整塊連續空間,用 Mark-Sweep 不壓縮,長期跑下來碎片嚴重會觸發退化的 Full GC。G1 把 Heap 切成 Region,回收時在 Region 之間做 Copying(等於自帶壓縮),不會有碎片問題。G1 還能設定停頓時間目標讓 JVM 自動調節每次回收的 Region 數量,CMS 沒有這種機制。
ZGC 為什麼能做到次毫秒停頓
傳統收集器需要 STW 來搬移物件並更新所有參照,這個步驟跟存活物件的數量成正比,Heap 越大停頓越長。ZGC 用 Colored Pointers 在指標裡記錄物件狀態,搭配 Load Barrier 讓應用程式在讀取參照時自動修正指標,因此搬移物件的過程可以跟應用程式並行進行。ZGC 只有在掃描 GC Roots 的時候需要極短暫的 STW,這個步驟跟 Heap 大小無關,所以不管 Heap 多大停頓都是次毫秒。
循環引用的物件會被回收嗎
這是經典的陷阱題。考慮以下情境:
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
// 切斷外部參照
a = null;
b = null;
// 此時兩個 Node 物件互相參照,但外面沒有人指向它們
答案是:會被回收。JVM 用的是 Reachability Analysis 而不是 Reference Counting。a = null; b = null; 之後,雖然兩個 Node 物件還互相參照,但從 GC Roots 已經走不到它們,形成一座「孤島」(Island of Isolation),GC 會把整座孤島判定為垃圾並回收。Java 不會遇到循環引用問題,這也是面試官用這題來測試候選人是否理解 JVM 回收機制的原因。

真正會被循環引用影響的是採用 Reference Counting 的語言。Python(CPython)的主要 GC 機制就是 Reference Counting,循環引用無法靠計數器歸零來回收,所以 CPython 額外實作了一個 cycle detector 來定期掃描並回收循環引用的物件。Objective-C 的 ARC(Automatic Reference Counting)也是 Reference Counting,開發者必須用 weak 參照手動打斷 retain cycle,否則就會 memory leak。JavaScript 的現代引擎(V8、SpiderMonkey)跟 Java 一樣採用 Mark-and-Sweep,不受循環引用影響。
怎麼排查 GC 造成的效能問題
開 GC 日誌是第一步,JDK 9 以後用 -Xlog:gc*。觀察重點包含:GC 停頓時間和頻率是否異常、Full GC 是否頻繁觸發、Heap 使用量在 GC 後有沒有真正降下來(如果降不下來,可能有 memory leak)。工具方面可以用 VisualVM、JDK Mission Control 或 GCEasy(線上分析 GC log)來視覺化觀察。
System.gc() 會立刻觸發 GC 嗎
不會,至少不保證。System.gc() 只是「建議」JVM 執行一次 Full GC,JVM 可以選擇忽略。HotSpot 預設會執行,但如果加了 -XX:+DisableExplicitGC 參數就會完全忽略。實務上不建議在程式碼裡呼叫 System.gc(),因為 Full GC 的 STW 停頓可能很長,而且 JVM 自己判斷觸發時機通常比開發者手動呼叫更合理。唯一常見的例外是 NIO 的 Direct Memory 不夠時,JVM 會內部呼叫 System.gc() 來觸發 Cleaner 回收 off-heap 記憶體,這時候如果設了 DisableExplicitGC 反而會造成 OutOfMemoryError。
Java 有 GC 為什麼還會 memory leak
GC 只能回收「從 GC Roots 不可達」的物件。如果一個物件已經沒用了但仍然被某條參照鏈指著,GC 就不會回收它,記憶體就洩漏了。常見的場景包含:
- 靜態集合:用
static List或static Map不斷往裡面塞東西卻不清理,物件跟著 class 的生命週期存活,直到 JVM 結束才釋放。 - 未關閉的資源:
InputStream、Connection、ResultSet沒有 close,底層的 native 資源持續佔用。用 try-with-resources 可以避免。 - Listener 和 Callback 沒移除:註冊了事件監聽器但忘了反註冊,持有監聽器的物件會一直被事件來源參照住。
- ThreadLocal:執行緒池的執行緒不會結束,
ThreadLocal裡存的物件會跟著執行緒一直活著。用完要呼叫remove()。 - 自訂的物件池或快取:用
HashMap做快取但沒有淘汰策略,物件只進不出。可以改用WeakHashMap或設定容量上限。
finalize() 的問題與替代方案
finalize() 是 Object 類別的方法,GC 判定物件不可達之後、真正回收之前會呼叫它,原本設計用來做資源清理。但這個機制問題很多:GC 不保證什麼時候呼叫(甚至不保證一定會呼叫)、有 finalize() 的物件回收需要兩次 GC 掃描才能完成(第一次標記後丟進 Finalizer Queue 等 Finalizer 執行緒處理,第二次才真正回收)、而且物件可以在 finalize() 裡把自己重新指派給一個 static 變數來「復活」,導致行為難以預測。
finalize() 從 Java 9 開始被標記為 deprecated,Java 18 進一步標記為 deprecated for removal。替代方案是 java.lang.ref.Cleaner(Java 9 引入),它用 PhantomReference 搭配獨立的清理執行緒,不會拖慢 GC、也不會讓物件復活。Cleaner 的運作原理、API 用法與常見錯誤在 Java Cleaner 與 PhantomReference 教學 有更詳細的說明。
Safe Point(安全點)
GC 在做 STW 的時候需要所有應用程式執行緒都暫停,但不是隨時都能暫停。執行緒必須跑到一個「安全點」(Safe Point)才能停下來,在安全點上 JVM 能準確知道每個物件參照的位置,不會漏標或誤標。常見的安全點包含方法呼叫、迴圈跳轉(backedge)、例外處理等位置。
如果某條執行緒長時間跑在一個沒有安全點的迴圈裡(例如 counted loop,JIT 編譯後可能不插入安全點),其他執行緒都停了卻要等這條跑完才能開始 GC,這就是 Time To Safe Point(TTSP)問題。JDK 17 之後可以用 -XX:+UseCountedLoopSafepoints 在 counted loop 裡也插入安全點來緩解,JDK 23 開始預設啟用。面試被問到 STW 機制時,能講出 Safe Point 的概念通常是加分項。
延伸閱讀
- Java 版本演進|從 Java 8 到 25 的語言進化史 — 各 LTS 版本的功能總覽
- Java 21 Virtual Threads 教學 — 虛擬執行緒如何改變 Java 的並行處理模式
- Java 25、26、27 新特性整理 — 最新三個版本的重點追蹤
- Java Cleaner 與 PhantomReference 教學 — 取代 finalize() 的資源清理機制
- HotSpot Virtual Machine Garbage Collection Tuning Guide(JDK 25) — Oracle 官方 GC 調校指南