Chuyển tới nội dung chính

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).

Tại sao Generational Hypothesis quan trọng?

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ùngVai trò
EdenNơ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ểmChi tiết
ScopeChỉ Young Generation (Eden + Survivors)
Tốc độRất nhanh (milliseconds)
Tần suấtThường xuyên (khi Eden đầy)
Stop-the-WorldCó, nhưng rất ngắn (< 10ms thường)
AlgorithmCopy live objects sang Survivor/Old Gen

Major GC / Full GC

Đặc điểmChi tiết
ScopeToà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-WorldCó, dài hơn Minor GC
Ảnh hưởngApplication 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ểmChi tiết
ThreadsSingle-thread
Phù hợpApp nhỏ, heap < 100MB, single-core
ƯuĐơn giản, overhead thấp nhất
NhượcPause dài cho heap lớn

Parallel GC

-XX:+UseParallelGC
Đặc điểmChi tiết
ThreadsMulti-thread (GC dùng nhiều cores)
Phù hợpBatch processing, throughput-focused
ƯuTối ưu throughput (tổng thời gian GC/tổng thời gian app)
NhượcPause 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ểmChi tiết
ApproachRegion-based, collect "garbage-first" (regions với nhiều garbage nhất)
Pause targetConfigurable: -XX:MaxGCPauseMillis=200 (default 200ms)
Phù hợpHầu hết applications, heap > 4GB
ƯuCân bằng throughput và latency
NhượcPhức tạp hơn, CPU overhead cao hơn Serial/Parallel

G1 Deep Dive: Regions và Collections

Region Types:

Loại RegionVai trò
EdenNơi objects mới được allocate
SurvivorObjects sống sót qua Young GC
OldObjects sống lâu (promoted từ Young)
HumongousObjects 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:MaxGCPauseMillistarget, không phải guarantee. G1 cố gắng đạt được 90% thời gian.

ZGC — Ultra-Low Latency

-XX:+UseZGC
Đặc điểmChi tiết
Pause timeSub-millisecond (< 1ms) bất kể heap size
Max heapLên đến 16 TB
Phù hợpLatency-critical apps (trading, real-time)
ƯuPause gần như không đáng kể
NhượcThroughput 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:

  1. 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) │
└────────────┴──────────────────────────────────┘
  1. 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
  2. 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?

ScenarioNê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ánhZGCShenandoah
Pause time< 1ms< 10ms (thường 1-5ms)
Công nghệColored pointersBrooks pointers (indirection pointers)
AvailabilityOracle JDK + OpenJDKChỉ OpenJDK (không có trong Oracle JDK)
Max heap16 TB1 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

CollectorPause TimeThroughputHeap SizeUse Case
SerialDàiTốt (single core)< 100MBApp nhỏ
ParallelTrung bìnhTốt nhấtTrung bìnhBatch processing
G1Ngắn (controllable)Tốt> 4GBHầu hết apps
ZGCRất ngắn (< 1ms)TốtRất lớnLatency-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
LatencyThời gian pause GCReal-time, interactive apps
FootprintMemory overhead của GCEmbedded, 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 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)
OCP Exam Tips

GC là topic thường gặp trong OCP Java Certification:

  1. Objects eligible for GC khi nào?

    • Khi không còn reachable references từ GC Roots
    • Đặt reference = null không guarantee ngay lập tức GC (chỉ làm object eligible)
  2. 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"
  3. 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
  4. 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)
  5. 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 qua c. 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:

  1. Batch data pipeline: Xử lý 10GB CSV mỗi đêm, không có user interaction
  2. Web API: 1000 requests/sec, p99 latency phải < 50ms
  3. Trading system: Mỗi microsecond đều quan trọng, heap 8GB
Xem lời giải
  1. Batch pipeline → Parallel GC: Ưu tiên throughput, không quan tâm latency. -XX:+UseParallelGC
  2. Web API → G1 GC: Cân bằng throughput và latency. -XX:+UseG1GC -XX:MaxGCPauseMillis=30
  3. 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 CollectionTự động giải phóng memory cho unreachable objects
GC RootsĐiểm bắt đầu reachability: local vars, static fields, threads
GenerationalHầu hết objects chết trẻ → tách Young/Old Gen
Young GenEden + Survivor S0/S1, Minor GC nhanh
Old GenObjects sống lâu, Full GC chậm
G1 GCDefault, region-based, pause target 200ms
ZGCSub-millisecond pauses, cho latency-critical
STWStop-the-World — tất cả app threads tạm dừng

Đọc thêm