Garbage Collection Deep Dive
Bài trước: Object Memory & String Pool — Bạn đã biết objects chiếm bao nhiêu memory trên Heap. Bài này giải thích cách JVM tự động dọn dẹp những objects không còn sử dụng.
Garbage Collection là gì?
Garbage Collection (GC) là cơ chế tự động quản lý memory trong Java. JVM phát hiện objects không còn ai reference đến (unreachable) và giải phóng memory:
void process() {
String temp = new String("Hello"); // Object tạo trên Heap
System.out.println(temp);
} // Sau method return, temp (reference) bị xóa khỏi Stack
// String "Hello" trên Heap trở thành unreachable → GC sẽ collect
Reachability — Khi nào object là "rác"?
GC bắt đầu từ GC Roots và đi theo references. Object không reachable từ bất kỳ root nào = garbage:
GC Roots:
├── Local variables trên Stack
├── Static fields
├── Active threads
└── JNI references
GC Root → A → B → C ← Reachable (sống)
└── D ← Reachable (sống)
E → F ← Unreachable (garbage!) — không ai từ GC Root trỏ đến
Object a = new Object(); // a → Object@1 (reachable qua local var a)
Object b = a; // b → Object@1 (reachable qua cả a và b)
a = null; // Object@1 vẫn reachable qua b
b = null; // Object@1 unreachable → garbage
Generational Hypothesis
Nền tảng thiết kế mọi GC hiện đại:
"Hầu hết objects chết trẻ" — Most objects die young.
Nghiên cứu thực tế cho thấy 90-95% objects sống rất ngắn (temporary variables, intermediate results). Chỉ một số ít sống lâu (caches, singletons, connections).
Nếu hầu hết objects chết trẻ → tách riêng vùng "trẻ" và "già". Quét vùng "trẻ" thường xuyên (rẻ, vì nhỏ) + quét vùng "già" ít hơn (đắt, nhưng hiếm khi cần).
Heap Generations
JVM chia Heap thành generations dựa trên tuổi thọ của object:
┌───────────────────────────────────────────────────┐
│ Heap │
├───────────────────────────┬───────────────────────┤
│ Young Generation │ Old Generation │
│ │ (Tenured) │
│ ┌───────┬──────┬──────┐ │ │
│ │ Eden │ S0 │ S1 │ │ Objects sống lâu │
│ │ │(Sur) │(Sur) │ │ (promoted từ Young) │
│ └───────┴──────┴──────┘ │ │
├───────────────────────────┴───────────────────────┤
│ Metaspace (ngoài Heap) │
│ Class metadata, static fields │
└───────────────────────────────────────────────────┘
Young Generation
| Vùng | Vai trò |
|---|---|
| Eden | Nơi objects mới được tạo |
| Survivor 0 (S0) | Buffer cho objects sống sót qua GC |
| Survivor 1 (S1) | Buffer xen kẽ với S0 |
Object Lifecycle
GC Events
Minor GC (Young GC)
| Đặc điểm | Chi tiết |
|---|---|
| Scope | Chỉ Young Generation (Eden + Survivors) |
| Tốc độ | Rất nhanh (milliseconds) |
| Tần suất | Thường xuyên (khi Eden đầy) |
| Stop-the-World | Có, nhưng rất ngắn (< 10ms thường) |
| Algorithm | Copy live objects sang Survivor/Old Gen |
Major GC / Full GC
| Đặc điểm | Chi tiết |
|---|---|
| Scope | Toàn bộ Heap (Young + Old + Metaspace) |
| Tốc độ | Chậm (hundreds of ms đến seconds) |
| Tần suất | Ít (khi Old Gen gần đầy) |
| Stop-the-World | Có, dài hơn Minor GC |
| Ảnh hưởng | Application bị "pause" — user có thể cảm nhận |
Stop-the-World (STW)
Stop-the-World = JVM tạm dừng tất cả application threads để GC hoạt động an toàn. Đây là lý do GC có thể gây "lag":
Application: ──────────╳════════╳──────────╳══════╳────────
│ STW │ │ STW │
│Minor GC │Minor │
│(2ms) │ │(3ms) │
Các Garbage Collectors
Serial GC
-XX:+UseSerialGC
| Đặc điểm | Chi tiết |
|---|---|
| Threads | Single-thread |
| Phù hợp | App nhỏ, heap < 100MB, single-core |
| Ưu | Đơn giản, overhead thấp nhất |
| Nhược | Pause dài cho heap lớn |
Parallel GC
-XX:+UseParallelGC
| Đặc điểm | Chi tiết |
|---|---|
| Threads | Multi-thread (GC dùng nhiều cores) |
| Phù hợp | Batch processing, throughput-focused |
| Ưu | Tối ưu throughput (tổng thời gian GC/tổng thời gian app) |
| Nhược | Pause vẫn có thể dài |
G1 GC (Garbage-First) — Default từ JDK 9
-XX:+UseG1GC
G1 chia Heap thành regions (các vùng nhỏ) thay vì generations cố định. Mỗi region có kích thước bằng nhau (1MB-32MB, mặc định tự động tính) và có thể là Eden, Survivor, Old, hoặc Humongous:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ S │ │ O │ O │ H │ │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ O │ │ E │ E │ │ O │ O │ E │
└───┴───┴───┴───┴───┴───┴───┴───┘
E = Eden, S = Survivor, O = Old, H = Humongous (large objects)
(blank) = Free
| Đặc điểm | Chi tiết |
|---|---|
| Approach | Region-based, collect "garbage-first" (regions với nhiều garbage nhất) |
| Pause target | Configurable: -XX:MaxGCPauseMillis=200 (default 200ms) |
| Phù hợp | Hầu hết applications, heap > 4GB |
| Ưu | Cân bằng throughput và latency |
| Nhược | Phức tạp hơn, CPU overhead cao hơn Serial/Parallel |
G1 Deep Dive: Regions và Collections
Region Types:
| Loại Region | Vai trò |
|---|---|
| Eden | Nơi objects mới được allocate |
| Survivor | Objects sống sót qua Young GC |
| Old | Objects sống lâu (promoted từ Young) |
| Humongous | Objects lớn ≥ 50% region size, allocate trực tiếp ở đây |
Young GC trong G1:
- G1 thu gom tất cả Eden regions + Survivor regions trong một lần GC
- Copy objects còn sống sang Survivor hoặc promote sang Old regions
- Stop-the-World, nhưng nhanh (vài ms đến chục ms)
Mixed Collections (Mixed GC):
G1 không chỉ làm Young GC. Khi Old Gen chiếm > InitiatingHeapOccupancyPercent (mặc định 45%), G1 chạy Mixed Collection:
- Thu gom Young regions + một phần Old regions (những Old regions có nhiều garbage nhất)
- Mục tiêu: dọn Old Gen dần dần, tránh Full GC đắt đỏ
Young GC: [Eden + Survivor] → collect
Mixed GC: [Eden + Survivor + Old (partial)] → collect
Full GC: [All regions] → collect (chỉ khi Mixed GC không đủ)
Humongous Objects:
Object lớn hơn 50% kích thước region (ví dụ: region 4MB, object ≥ 2MB) được coi là Humongous. G1 allocate chúng trực tiếp vào contiguous regions (nhiều regions liên tiếp) trong Old Gen. Lưu ý: quá nhiều Humongous objects → overhead cao → cân nhắc tăng -XX:G1HeapRegionSize.
Remembered Sets (RSets): Mỗi region có một RSet (Remembered Set) lưu references từ regions khác trỏ vào nó. Khi GC một region, G1 chỉ cần scan RSet thay vì toàn bộ heap → giảm thời gian pause.
Region A (Old) → object trong Region B (Young)
→ RSet của Region B ghi nhận: "Region A references me"
→ Khi GC Region B, chỉ scan RSet + GC Roots, không scan toàn bộ Old Gen
G1 Ergonomics — Tự điều chỉnh:
G1 có cơ chế ergonomics (tự điều chỉnh) để đáp ứng -XX:MaxGCPauseMillis target:
- Nếu pause > target: giảm số Young regions để GC nhanh hơn
- Nếu pause < target: tăng số Young regions để cải thiện throughput
- Lưu ý:
-XX:MaxGCPauseMillislà target, không phải guarantee. G1 cố gắng đạt được 90% thời gian.
ZGC — Ultra-Low Latency
-XX:+UseZGC
| Đặc điểm | Chi tiết |
|---|---|
| Pause time | Sub-millisecond (< 1ms) bất kể heap size |
| Max heap | Lên đến 16 TB |
| Phù hợp | Latency-critical apps (trading, real-time) |
| Ưu | Pause gần như không đáng kể |
| Nhược | Throughput thấp hơn G1 một chút, memory overhead cao hơn |
ZGC Deep Dive: Concurrent Collection
ZGC là collector thế hệ mới (production-ready từ Java 15) với mục tiêu chính: pause time < 1ms bất kể heap size. Điều này thay đổi game cho applications cần latency cực thấp.
Công nghệ cốt lõi:
- Colored Pointers (con trỏ màu):
- ZGC sử dụng 64-bit pointers (yêu cầu 64-bit JVM) và "tô màu" các bits cao để đánh dấu trạng thái object
- Ví dụ: 4 bits cao dùng cho metadata (marked, remapped, finalizable, etc.)
- Kỹ thuật này cho phép ZGC track object state không cần Stop-the-World
64-bit pointer trong ZGC:
┌────────────┬──────────────────────────────────┐
│ Metadata │ Actual object address │
│ (4 bits) │ (42-60 bits depending on OS) │
└────────────┴──────────────────────────────────┘
-
Load Barriers (rào cản tải):
- Mỗi khi application đọc object reference từ heap, ZGC chèn một load barrier (kiểm tra nhỏ)
- Load barrier kiểm tra colored bits → nếu object đã bị relocate (di chuyển), tự động forward pointer
- Trade-off: ~4% overhead CPU, nhưng đổi lại pause time < 1ms
-
Concurrent Relocation (di chuyển đồng thời):
- ZGC di chuyển objects (compaction) trong khi app đang chạy, không cần STW
- Application threads và GC threads cùng hoạt động
- Load barriers đảm bảo app luôn thấy đúng địa chỉ object
ZGC Phases:
STW Pause: ╳ (0.01ms - 0.5ms) — chỉ scan roots
│
Concurrent: ════════════════════ — mark, relocate, remap (app chạy song song)
│
STW Pause: ╳ (0.01ms - 0.5ms) — final cleanup
Khi nào dùng ZGC?
| Scenario | Nên dùng ZGC? |
|---|---|
| Heap > 100GB | ✅ Có — G1/Parallel sẽ pause lâu |
| Latency yêu cầu < 10ms | ✅ Có — ZGC đảm bảo < 1ms |
| Batch processing, không quan tâm latency | ❌ Không — dùng Parallel GC cho throughput cao hơn |
| Multi-tenant applications | ✅ Có — tránh "noisy neighbor" do GC pause |
Cách bật ZGC:
# Java 15+ (production-ready)
java -XX:+UseZGC -Xmx16g -jar app.jar
# Kết hợp với GC logging để theo dõi
java -XX:+UseZGC -Xlog:gc*:file=zgc.log -Xmx16g -jar app.jar
Lưu ý:
- ZGC yêu cầu 64-bit JVM (không hỗ trợ 32-bit)
- Memory overhead ~10-15% cao hơn G1 (do colored pointers, metadata)
- Throughput thấp hơn Parallel GC ~5-10%, nhưng latency tốt hơn rất nhiều
Shenandoah GC — Concurrent Collector khác
-XX:+UseShenandoahGC
Shenandoah là một low-pause collector khác, tương tự ZGC:
| So sánh | ZGC | Shenandoah |
|---|---|---|
| Pause time | < 1ms | < 10ms (thường 1-5ms) |
| Công nghệ | Colored pointers | Brooks pointers (indirection pointers) |
| Availability | Oracle JDK + OpenJDK | Chỉ OpenJDK (không có trong Oracle JDK) |
| Max heap | 16 TB | 1 TB (thực tế) |
| Throughput | ~95% G1 | ~97% G1 |
Brooks Pointers: Khác với ZGC dùng colored pointers, Shenandoah dùng Brooks pointers (forwarding pointers):
- Mỗi object có thêm một indirection pointer trỏ đến chính nó (hoặc vị trí mới nếu bị relocate)
- Overhead memory ~1-2% (ít hơn ZGC), nhưng pause time cao hơn ZGC một chút
Khi nào dùng Shenandoah?
- Bạn dùng OpenJDK (không phải Oracle JDK)
- Cần low latency nhưng heap < 1TB
- Muốn throughput cao hơn ZGC một chút (~2%)
# OpenJDK only
java -XX:+UseShenandoahGC -Xmx8g -jar app.jar
So sánh tổng quan
| Collector | Pause Time | Throughput | Heap Size | Use Case |
|---|---|---|---|---|
| Serial | Dài | Tốt (single core) | < 100MB | App nhỏ |
| Parallel | Trung bình | Tốt nhất | Trung bình | Batch processing |
| G1 | Ngắn (controllable) | Tốt | > 4GB | Hầu hết apps |
| ZGC | Rất ngắn (< 1ms) | Tốt | Rất lớn | Latency-critical |
GC Metrics: Trade-offs
Không có GC "tốt nhất" cho mọi trường hợp — luôn có trade-off:
Throughput ←──────────────→ Latency
↑ ↑
(Parallel GC) (ZGC)
↑
(G1 GC)
← Footprint (memory usage) →
| Metric | Ý nghĩa | Ưu tiên khi |
|---|---|---|
| Throughput | % thời gian app chạy (vs GC chạy) | Batch processing, data pipeline |
| Latency | Thời gian pause GC | Real-time, interactive apps |
| Footprint | Memory overhead của GC | Embedded, container (limited memory) |
Practical: JVM Flags cho GC
Cấu hình Heap Size
# Initial heap 256MB, max heap 2GB
java -Xms256m -Xmx2g -jar app.jar
# Best practice: set Xms = Xmx để tránh resize overhead
java -Xms2g -Xmx2g -jar app.jar
Chọn Garbage Collector
java -XX:+UseG1GC -jar app.jar # G1 (default từ JDK 9)
java -XX:+UseZGC -jar app.jar # ZGC
java -XX:+UseParallelGC -jar app.jar # Parallel
GC Tuning Flags — Điều chỉnh nâng cao
Cảnh báo: Thường thì default settings đã tốt rồi. Chỉ tune khi:
- Bạn có metrics cụ thể (GC logs, profiling) chứng minh có vấn đề
- Bạn hiểu rõ trade-offs (pause time ↔ throughput ↔ memory)
G1 Tuning Flags:
# Target pause time 100ms (default 200ms)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
# Đặt kích thước region (default tự động, thường 1-32MB)
# Nếu có nhiều Humongous objects, tăng region size
java -XX:+UseG1GC -XX:G1HeapRegionSize=4m -jar app.jar
# Bắt đầu marking khi Old Gen chiếm X% heap (default 45%)
# Giảm xuống 35% nếu thấy Full GC xảy ra
java -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=35 -jar app.jar
# Số lượng threads GC song song (default = CPU cores / 4)
java -XX:+UseG1GC -XX:ParallelGCThreads=4 -jar app.jar
# Kết hợp
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:InitiatingHeapOccupancyPercent=40 \
-XX:G1HeapRegionSize=4m \
-Xms4g -Xmx4g \
-jar app.jar
ZGC Tuning Flags:
# ZGC ít flags hơn (tự điều chỉnh nhiều)
# Số concurrent GC threads (default = CPU cores / 8)
java -XX:+UseZGC -XX:ConcGCThreads=2 -jar app.jar
# Soft max heap (ZGC cố giữ dưới mức này)
java -XX:+UseZGC -Xmx16g -XX:SoftMaxHeapSize=12g -jar app.jar
Khi KHÔNG nên tune:
- Application chạy tốt rồi → don't fix what ain't broken
- Chưa có GC logs/metrics → measure first, tune later
- Full GC chưa từng xảy ra → default đang hoạt động tốt
GC Logging — Bật logs để phân tích
# Java 9+ unified logging (cú pháp mới)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
# Output mẫu:
# [0.123s] GC(0) Pause Young (Normal) 24M->8M(256M) 2.5ms
# [1.456s] GC(1) Pause Young (Normal) 32M->12M(256M) 3.1ms
# Chi tiết hơn (debug level)
java -Xlog:gc*=debug:file=gc-debug.log:time,uptime,level,tags -jar app.jar
# Chỉ log GC pauses (không log details)
java -Xlog:gc:file=gc.log -jar app.jar
Phân tích GC logs:
- Dùng công cụ như GCEasy (gceasy.io), GCViewer để visualize
- Tìm: Full GC frequency, pause times (p50, p99, max), throughput
Lỗi thường gặp
Lỗi 1: Gọi System.gc() để "force" GC
// ❌ System.gc() chỉ là "gợi ý" — JVM có thể bỏ qua
System.gc(); // Không đảm bảo GC sẽ chạy
// ✅ Để JVM tự quyết định khi nào GC
// Oracle: "Calling System.gc() is almost always a bad idea"
Lỗi 2: Set Xmx quá lớn nghĩ "nhiều hơn = tốt hơn"
# ❌ Heap 32GB → Full GC sẽ rất lâu (có thể seconds)
java -Xmx32g -XX:+UseParallelGC -jar app.jar
# ✅ Set vừa đủ + dùng GC phù hợp
java -Xmx4g -XX:+UseG1GC -jar app.jar
# Hoặc dùng ZGC cho heap lớn
java -Xmx32g -XX:+UseZGC -jar app.jar
Lỗi 3: Không phân biệt Minor GC và Full GC
- Minor GC: nhanh, thường xuyên, bình thường → OK
- Full GC: chậm, nên hiếm → Nếu xảy ra thường xuyên = vấn đề (Old Gen đầy, cần điều tra)
GC là topic thường gặp trong OCP Java Certification:
-
Objects eligible for GC khi nào?
- Khi không còn reachable references từ GC Roots
- Đặt reference =
nullkhông guarantee ngay lập tức GC (chỉ làm object eligible)
-
System.gc()là gì?- Chỉ là suggestion (gợi ý), không phải guarantee
- JVM có thể bỏ qua hoàn toàn
- Đáp án đúng: "It suggests that the JVM run garbage collection"
-
finalize()method- Deprecated từ Java 9 (không nên dùng)
- Được gọi trước khi object bị GC (nếu JVM quyết định gọi)
- Không guarantee chạy → không nên dùng để cleanup resources
- Thay thế: dùng
try-with-resources+AutoCloseable
-
Câu hỏi điển hình:
String s = new String("hello");
s = null;
// s eligible for GC? → YES (nếu không có reference nào khác)Object a = new Object();
Object b = a;
a = null;
// Object eligible for GC? → NO (vẫn còn b reference) -
GC không thu gom gì?
- Không thu gom Stack memory (tự động cleared khi method return)
- Không thu gom Metaspace (class metadata) — chỉ khi class unload
Mẹo nhớ: "Reachable = sống, Unreachable = chết (eligible for GC)"
Bài tập
Bài 1: GC Basics [Cơ bản]
Cho đoạn code sau, xác định tại mỗi comment: bao nhiêu objects eligible for GC?
public static void main(String[] args) {
Object a = new Object(); // Line 1
Object b = new Object(); // Line 2
Object c = a; // Line 3
a = null; // Line 4 — eligible for GC: ?
b = null; // Line 5 — eligible for GC: ?
c = null; // Line 6 — eligible for GC: ?
}
Xem lời giải
- Line 4:
a = null→ Object@1 vẫn reachable quac. 0 eligible. - Line 5:
b = null→ Object@2 unreachable. 1 eligible (Object@2). - Line 6:
c = null→ Object@1 unreachable. 2 eligible (Object@1 + Object@2).
Bài 2: Chọn GC phù hợp [Trung bình]
Cho 3 scenarios, chọn GC collector phù hợp nhất và giải thích:
- Batch data pipeline: Xử lý 10GB CSV mỗi đêm, không có user interaction
- Web API: 1000 requests/sec, p99 latency phải < 50ms
- Trading system: Mỗi microsecond đều quan trọng, heap 8GB
Xem lời giải
- Batch pipeline → Parallel GC: Ưu tiên throughput, không quan tâm latency.
-XX:+UseParallelGC - Web API → G1 GC: Cân bằng throughput và latency.
-XX:+UseG1GC -XX:MaxGCPauseMillis=30 - Trading → ZGC: Ultra-low latency, sub-millisecond pauses.
-XX:+UseZGC
Bài 3: GC Log Analysis [Thách thức]
Phân tích GC log sau và trả lời: (a) GC collector nào? (b) Young Gen trước/sau GC? (c) Tổng heap trước/sau? (d) Pause time?
[2024-01-15T10:30:00.123+0700] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
[2024-01-15T10:30:00.123+0700] GC(42) Eden regions: 24->0(24)
[2024-01-15T10:30:00.123+0700] GC(42) Survivor regions: 3->3(4)
[2024-01-15T10:30:00.123+0700] GC(42) Old regions: 15->15
[2024-01-15T10:30:00.123+0700] GC(42) Humongous regions: 2->2
[2024-01-15T10:30:00.128+0700] GC(42) Pause Young (Normal) 352M->160M(512M) 5.123ms
Xem lời giải
(a) G1 GC (G1 Evacuation Pause) (b) Eden: 24 regions → 0 (tất cả cleared). Survivor: 3 → 3 (sống sót được copy) (c) Tổng heap: 352MB → 160MB (freed ~192MB), max heap 512MB (d) Pause time: 5.123ms
Nhận xét: Minor GC bình thường, pause 5ms chấp nhận được. Eden cleared hoàn toàn (tốt — objects chết trẻ). Old Gen không thay đổi (không có promotion lần này).
Tóm tắt
| Khái niệm | Điểm chính |
|---|---|
| Garbage Collection | Tự động giải phóng memory cho unreachable objects |
| GC Roots | Điểm bắt đầu reachability: local vars, static fields, threads |
| Generational | Hầu hết objects chết trẻ → tách Young/Old Gen |
| Young Gen | Eden + Survivor S0/S1, Minor GC nhanh |
| Old Gen | Objects sống lâu, Full GC chậm |
| G1 GC | Default, region-based, pause target 200ms |
| ZGC | Sub-millisecond pauses, cho latency-critical |
| STW | Stop-the-World — tất cả app threads tạm dừng |