GC Modern Deep — G1, ZGC, Shenandoah: cơ chế, tradeoff và khi nào chọn
Đào sâu 3 GC hiện đại: G1 region-based SATB, ZGC colored pointer và load barrier, Shenandoah Brooks pointer. Write barrier vs read barrier. Khi nào chọn GC nào cho latency và throughput.
TL;DR: Bài 05 cho thấy G1 là default Java 9+ và ZGC đạt pause dưới 10ms. Bài này đào sâu cơ chế: G1 dùng region-based heap + SATB (Snapshot-At-The-Beginning) concurrent marking + Remembered Set để track inter-region reference. ZGC dùng colored pointer (3 bit màu trong 64-bit pointer) + load barrier (check màu khi đọc reference) để relocate object hoàn toàn concurrent. Shenandoah (RedHat) dùng Brooks pointer (indirection layer) + read barrier để compact concurrent. Write barrier (G1) — tốn khi ghi, free khi đọc. Read barrier (ZGC, Shenandoah) — tốn khi đọc nhưng cho phép concurrent compaction. Overhead thực tế: ZGC/Shenandoah 10-20% throughput thấp hơn G1. Chọn dựa trên SLA latency và heap size.
Bài 05 nói về GC overview ở mức "vì sao chia young/old, cách đọc GC log, khi nào chọn Parallel vs G1 vs ZGC". Bài này là deep dive: cơ chế kỹ thuật bên trong từng collector, lý do design khác nhau, và tradeoff chi tiết. Mục tiêu: có đủ nền tảng để đọc GC tuning guide, hiểu GC log chi tiết, và lý luận về GC selection thay vì chỉ copy-paste recommendation.
1. Scenario — SLA 99.9% latency vượt ngưỡng do Full GC đêm
Service recommendation API, SLA 99.9% latency dưới 100ms. Heap 24GB, dùng G1 default. Mỗi đêm khoảng 2h sáng:
[02:14:33][gc] GC(187) Pause Full (Ergonomics) 24576M->18432M(24576M) 847.234ms
Full GC 847ms gây breach. 99.9% latency trong 1 giờ = 3.6 giây budget, 847ms GC xài hết 23% budget trong 1 lần. Cần giảm max pause xuống dưới 10ms. Lựa chọn?
Câu trả lời không đơn giản là "switch sang ZGC". Cần hiểu tại sao Full GC xảy ra với G1, ZGC fix bằng cơ chế gì, và trade-off sẽ là gì sau khi switch. Bài này trang bị đủ để trả lời.
2. Region-based GC — nền tảng chung
Region-based GC là paradigm phổ biến trong cả 3 collector: G1, ZGC, Shenandoah. Thay vì chia heap thành 2 half (young/old cứng nhắc), heap được chia thành nhiều region nhỏ.
Tại sao region-based tốt hơn fixed generation:
- Partial collection: GC có thể chọn collect 1 subset region (những region có nhiều rác nhất — "garbage first") thay vì phải scan toàn heap. Giảm thời gian mỗi GC cycle.
- Flexible assignment: region có thể đổi role (Eden → Survivor → Old → Free) tuỳ nhu cầu. Không bị cứng nhắc "old phải chiếm 70%".
- Concurrent work: GC thread có thể làm việc trên 1 region trong khi app thread dùng region khác.
- Heap fragmentation: compact từng region nhỏ dễ hơn compact toàn heap.
| GC | Region size | Số region | Đặc điểm |
|---|---|---|---|
| G1 | 1-32 MB (tự tính) | ~2048 | Fixed size, label Eden/Survivor/Old/Humongous |
| ZGC | 2 MB / 32 MB / lớn hơn | Linh hoạt | 3 loại: small/medium/large |
| Shenandoah | Cấu hình được | ~2000 | Mặc định 256KB-32MB |
3. G1 GC — cơ chế SATB và Remembered Set
G1 (Garbage-First), default Java 9+, dùng 3 structure kỹ thuật quan trọng:
SATB — Snapshot-At-The-Beginning
SATB là thuật toán concurrent marking: trước khi bắt đầu marking phase, JVM chụp snapshot trạng thái reference graph. Mọi object live tại thời điểm snapshot sẽ được coi là live trong GC cycle này, ngay cả khi app thread xoá reference sau đó.
Tại sao cần SATB: nếu GC thread đang mark object A, app thread đồng thời xoá reference A→B và thêm reference C→B, GC thread có thể bỏ sót B (chưa scan C). SATB đảm bảo: nếu B live lúc bắt đầu marking, B được mark live — kể cả khi reference đến B thay đổi trong quá trình mark.
Write barrier của G1 (SATB write barrier): mỗi khi app thread ghi vào reference field, barrier ghi giá trị cũ của field vào SATB queue. GC thread đọc queue và mark các object cũ là live. Chi phí: mỗi reference write thêm 1 buffer write (~5-10 ns, amortized).
App thread: field.ref = newObject // write
SATB barrier: satb_queue.push(field.ref.old) // ghi gia tri cu vao queue
GC thread: process satb_queue -> mark old objects live
Remembered Set (RSet)
Remembered Set là cấu trúc per-region ghi lại các reference từ region khác vào region đó. Khi GC collect region R, GC cần biết region nào ngoài R có pointer vào R — để update sau khi compact. Thay vì scan toàn heap (tốn O(heap)), GC scan RSet (tốn O(RSet size)).
RSet được maintain bởi card table write barrier: mỗi khi reference field được ghi, JVM đánh dấu 1 card (512 byte chunk) trong card table. Refinement thread (chạy concurrent) đọc card table và update RSet.
Chi phí G1:
- Write barrier: ~5-10 ns mỗi reference write.
- Refinement thread: ~1 core liên tục.
- RSet overhead: ~5-10% extra memory.
G1 GC cycle
flowchart LR
A[Young GC - STW\nEvacuate Eden + Survivor] --> B[Concurrent Marking\nSATB - app runs]
B --> C[Remark - short STW\nDrain SATB queues]
C --> D[Cleanup - short STW\nIdentify empty regions]
D --> E[Mixed GC - STW\nYoung + high-garbage Old]
E --> F{Old region\npressure?}
F -- Low --> A
F -- High -- Full GC trigger --> G[Full GC - long STW\nMark-compact entire heap]Full GC trong G1 xảy ra khi Mixed GC không kịp dọn Old gen (allocation vượt evacuation speed). G1 fallback sang single-thread mark-compact toàn heap — đây là source của 847ms pause trong scenario đầu bài. Bài 09 giải thích tại sao TTSP còn cộng thêm vào nữa.
Chống Full GC G1:
# Giam threshold start concurrent marking som hon
-XX:InitiatingHeapOccupancyPercent=35 # default 45
# Tang so GC thread
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=4
# Tang heap hoac giam allocation rate
4. ZGC — colored pointer và load barrier
ZGC (Z Garbage Collector), production Java 15+, Generational Java 21+ (JEP 439). Mục tiêu: pause dưới 10ms với mọi heap size, từ vài GB đến 16TB.
Cơ chế cốt lõi của ZGC khác biệt hoàn toàn với G1: thay vì dùng write barrier để track mutation, ZGC dùng read barrier để detect và fix stale pointer khi đọc reference.
Colored pointer — 3 bit màu trong pointer
ZGC nhúng metadata vào pointer 64-bit bằng cách sử dụng các bit cao (không dùng bởi OS do địa chỉ thực tế dùng 44-48 bit trên Linux x86-64):
64-bit reference trong ZGC (Linux x86-64):
Bit: [63...46][45][44][43][42][41...0]
| | | | | |
unused(18) bad fin remap mark1 mark0 address(42)
bad : pointer đang invalid (debugging)
finalizable: đã finalize
remapped : đã được relocate và pointer đã update
marked0/1: đã được mark live trong marking phase (alternating bit)
3 bit quan trọng nhất trong GC cycle: marked, remapped, finalizable. Giá trị của các bit này xác định trạng thái của object trong GC phase hiện tại.
Load barrier — check khi đọc reference
Mỗi khi app code đọc một reference từ heap (load object reference từ field của object khác), JIT chèn một load barrier nhỏ:
; x86 pseudo-assembly - ZGC load barrier
mov rax, [rbx + offset] ; load reference normally
test rax, [bad_mask] ; check "bad" bits (metadata)
jnz slow_path ; if bits set -> need fixup
; fast path: reference is good, continue
Nếu metadata bits set (pointer cũ, cần update):
- Slow path: JVM xác định địa chỉ mới của object (từ forwarding table), update pointer, trả về pointer mới cho app thread. App thread tiếp tục với pointer đúng — không biết fixup xảy ra.
Load barrier overhead: ~5-10% throughput (mỗi reference load có thêm check). Đây là trade-off cốt lõi của ZGC.
ZGC phases — gần hoàn toàn concurrent
flowchart LR
A["Pause: Mark Start ~1ms\n(scan GC roots, set mark bit)"] --> B
B["Concurrent Mark\n(traverse heap, mark live objects)"] --> C
C["Pause: Mark End ~1ms\n(drain mark queues, finalize)"] --> D
D["Concurrent Reference Processing\n(weakRef, phantom, finalization)"] --> E
E["Concurrent Relocation Set Selection\n(choose regions to compact)"] --> F
F["Concurrent Relocation\n(copy live objects, update forwarding table)"] --> G
G["Concurrent Remapping\n(update all pointers)"] --> H[End cycle]Chỉ 2 pause: Mark Start (~1ms) và Mark End (~1ms). Tổng pause per cycle thường dưới 2ms. Mọi work thực (traverse heap, copy object, update pointer) đều concurrent với app.
Forwarding table: khi GC copy object từ địa chỉ cũ sang địa chỉ mới, GC lưu mapping old_addr → new_addr trong forwarding table. Load barrier tra cứu table này khi gặp pointer cũ (remapped bit = 0). Sau khi tất cả pointer được update (remapping phase), forwarding table được free.
Generational ZGC (Java 21, JEP 439)
ZGC trước Java 21 không có generational — không tận dụng generational hypothesis (phần lớn object chết trẻ). Tất cả object trong cùng 1 GC space.
Java 21 thêm generational ZGC: Young generation + Old generation, giống G1. Young GC thường xuyên hơn, cheap hơn. Old GC concurrent. Throughput cải thiện đáng kể so với non-generational ZGC, trong khi giữ pause dưới 10ms.
# Java 21+: generational ZGC mac dinh
java -XX:+UseZGC -Xmx16g MyApp
# Java 17-20: non-generational ZGC
java -XX:+UseZGC -Xmx16g MyApp
5. Shenandoah — Brooks pointer và concurrent compaction
Shenandoah (RedHat, JEP 189, OpenJDK Java 12+). Mục tiêu tương tự ZGC: pause dưới 10ms. Approach khác.
Brooks pointer — indirection per object
Shenandoah thêm 1 word (8 byte) vào mỗi object: Brooks pointer (tên theo nhà nghiên cứu Rodney A. Brooks). Brooks pointer bình thường trỏ đến chính object đó (self-pointer). Khi GC relocate object sang địa chỉ mới, Brooks pointer cũ được update để trỏ đến địa chỉ mới.
Before relocation:
Object A at addr 0x1000:
[brooks_ptr → 0x1000][mark_word][klass_ptr][fields...]
(self-pointer, no relocation)
After relocation (GC copies A to 0x5000):
Object A at addr 0x1000 (old):
[brooks_ptr → 0x5000][mark_word][klass_ptr][fields...] <- forwarding
Object A at addr 0x5000 (new, canonical):
[brooks_ptr → 0x5000][mark_word][klass_ptr][fields...] <- actual data
Mọi reference đọc đến object A đều đi qua Brooks pointer — dù đọc từ địa chỉ cũ (0x1000) hay mới (0x5000), đều nhận về data đúng tại 0x5000.
Read barrier: mỗi lần đọc reference, Shenandoah follow Brooks pointer. Chi phí: 1 extra pointer dereference per load. Tương tự ZGC overhead.
Ưu điểm Brooks pointer so với ZGC colored pointer:
- Không phụ thuộc bit cao của pointer — hoạt động trên kiến trúc 32-bit hoặc OS dùng hết 48 bit địa chỉ.
- Không cần thay đổi pointer encoding.
Nhược điểm: mỗi object tốn thêm 8 byte (Brooks pointer). Heap overhead ~5-15% tuỳ object size average.
Shenandoah "elimination optimization" (Java 13+): JIT nhận ra pattern self-pointer và có thể elide Brooks pointer dereference trong nhiều trường hợp — giảm overhead load barrier. Bài whitepaper gọi là "Brooks pointer elimination".
Generational Shenandoah (Java 21 preview)
Tương tự Generational ZGC, thêm young/old generation để giảm GC pressure. Experimental trong Java 21+, production-ready dự kiến Java 23-24.
6. Write barrier vs Read barrier — tradeoff cơ bản
Đây là phân chia quan trọng nhất giữa GC modern:
Write barrier (G1, CMS legacy):
- Tốn chi phí khi ghi reference field (
obj.field = other). - Đọc reference: free (không có overhead).
- Dùng để: track mutation (SATB), maintain Remembered Set (card table).
- Phù hợp với: workload đọc nhiều hơn ghi (read-heavy).
Read barrier (ZGC, Shenandoah):
- Tốn chi phí khi đọc reference field (
x = obj.field). - Ghi reference: chi phí thấp hơn (không cần full write barrier).
- Dùng để: detect stale pointer khi load, trigger fixup, cho phép concurrent relocation.
- Overhead cao hơn vì đọc reference xảy ra nhiều hơn ghi nhiều lần trong code thực tế.
- Nhưng: cho phép concurrent compaction mà write barrier không làm được.
| Barrier type | Chi phí ghi | Chi phí đọc | Cho phép concurrent compaction |
|---|---|---|---|
| Write barrier (G1) | ~5-10 ns/write | 0 | Không |
| Read barrier (ZGC/Shenandoah) | ~1-2 ns/write | ~3-8 ns/read | Có |
Tại sao read barrier cần cho concurrent compaction:
Khi GC muốn copy object đang được app thread dùng, cần đảm bảo app thread luôn thấy data đúng. Write barrier không đủ — app thread đọc reference cũ (không qua barrier) vẫn thấy địa chỉ cũ. Read barrier đảm bảo mọi load đều qua barrier → có thể intercept và redirect sang địa chỉ mới. Compaction có thể xảy ra concurrent.
flowchart LR
subgraph WriteBarrier[Write Barrier - G1]
W1[App: obj.field = X] --> W2[Barrier: record old value]
W3[App: y = obj.field] --> W4[No barrier - direct load]
W4 --> W5[GC cannot move obj while app reads it]
end
subgraph ReadBarrier[Read Barrier - ZGC]
R1[App: obj.field = X] --> R2[Minimal barrier]
R3[App: y = obj.field] --> R4[Barrier: check color bits]
R4 --> R5[If relocated: return new address]
R5 --> R6[GC CAN move obj concurrently]
end7. So sánh 4 collector
| Tiêu chí | Parallel GC | G1 GC | ZGC | Shenandoah |
|---|---|---|---|---|
| Default | Java 8 server | Java 9-20 server | Java 21+ option | Opt-in |
| Pause target | Không có | 200ms (soft) | dưới 10ms | dưới 10ms |
| Pause thực tế | 100ms-10s | 20-500ms | dưới 2ms thường | dưới 10ms |
| Throughput overhead | 0% (baseline) | ~5% vs Parallel | ~10-15% | ~10-15% |
| Memory overhead | ~0% | ~10% (RSet) | ~10-20% (forwarding) | ~5-15% (Brooks ptr) |
| Heap range tốt nhất | 1-32 GB | 4-32 GB | 8 MB-16 TB | 200 MB-multi TB |
| Concurrent mark | Không | Có | Có | Có |
| Concurrent compact | Không | Không (STW compact) | Có (load barrier) | Có (Brooks pointer) |
| Generational | Có | Có | Java 21+ | Java 21 preview |
| Barrier type | Minimal | Write barrier | Read barrier | Read barrier |
8. Khi nào chọn GC nào
flowchart TD
A[Bắt đầu] --> B{Workload type?}
B -- Batch / offline / ETL --> C[Parallel GC]
B -- Web / microservice --> D{SLA latency p99?}
D -- Vượt 200ms OK --> E[G1 - default, it tune]
D -- Dưới 100ms --> F{Heap size?}
F -- Dưới 32 GB --> G[G1 well-tuned hoac ZGC]
F -- Vượt 32 GB --> H[ZGC]
D -- Dưới 10ms --> H
H --> I{Java version?}
I -- Java 21+ --> J[Generational ZGC - uu tien]
I -- Java 17-20 --> K[ZGC non-gen hoac Shenandoah]Parallel GC: batch job, ETL, scientific compute, offline analytics. Throughput tối đa, pause chấp nhận được (không có user request). -XX:+UseParallelGC.
G1 GC: web service, microservice, API server với heap 4-32 GB. Default Java 9+. Pause 50-200ms acceptable cho SLA 99th percentile 500ms. Không cần tune nhiều — default thường đủ tốt. -XX:+UseG1GC.
ZGC: latency-critical service (trading platform, real-time bidding, gaming backend), heap lớn (vượt 32 GB), SLA p99 dưới 10ms. Java 21+ dùng Generational ZGC cho throughput tốt hơn. -XX:+UseZGC.
Shenandoah: tương tự ZGC về latency target. Phù hợp nếu kiến trúc không support ZGC colored pointer (32-bit system, special OS), hoặc prefer RedHat support. -XX:+UseShenandoahGC.
Throughput overhead 10-15% nghĩa là với cùng workload, cần thêm ~15% CPU hoặc server để bù. Memory overhead 10-20% nghĩa là với heap 16 GB, cần reserve 18-20 GB thực tế. Đo benchmark trên workload thực trước khi switch production. Đừng assume "ZGC = always better".
9. Pitfall — các sai lầm phổ biến khi chọn và tune GC
Pitfall 1 — Switch sang ZGC mà không bù throughput overhead:
# Truoc: G1, 10 server x 16 vCPU, throughput 100k RPS
java -XX:+UseG1GC -Xmx24g MyApp
# Sau: ZGC, cung 10 server, throughput 85k RPS (giam 15%)
java -XX:+UseZGC -Xmx24g MyApp
# Dung: can them 2 server (12 server) hoac scale CPU de bu overhead
Switch sang ZGC mà không đánh giá throughput drop → capacity shortage → latency tăng ironically.
Pitfall 2 — G1 Full GC do allocation spike:
Pattern: alloc rate binh thuong 500 MB/s, spike len 2 GB/s (batch import)
G1 concurrent marking khong kip doc -> Old day -> Full GC
Fix:
-XX:InitiatingHeapOccupancyPercent=35 # Start marking som hon (default 45)
-XX:G1ReservePercent=15 # Giu 15% heap buffer (default 10)
-XX:ConcGCThreads=4 # Tang GC thread (default cores/4)
Hoặc tách batch import ra process riêng với Parallel GC, không để batch spike ảnh hưởng online service.
Pitfall 3 — Humongous object trong G1:
G1 có region size (1-32 MB). Object lớn hơn 50% region size → Humongous region — cả region dành cho 1 object. Humongous region không được GC trong Young GC thông thường, chỉ được clean trong concurrent cycle. Nhiều Humongous object → Old gen pressure → Full GC sớm.
// Pitfall: buffer 50MB trong G1 voi region size 32MB
byte[] buffer = new byte[50 * 1024 * 1024]; // Humongous!
// Fix 1: dung DirectByteBuffer (off-heap)
ByteBuffer direct = ByteBuffer.allocateDirect(50 * 1024 * 1024);
// Fix 2: tang region size (giam so Humongous)
// -XX:G1HeapRegionSize=64m (mac dinh 1-32 tuy heap size)
Pitfall 4 — ZGC và heap quá nhỏ:
ZGC overhead là overhead absolute — không scale tốt với heap nhỏ. Heap 512 MB với ZGC: overhead 10-20% = 50-100 MB lost. Với G1, overhead cũng tương tự nhưng GC cycle nhanh hơn vì heap nhỏ.
# Heap < 2 GB: G1 hoac Serial GC thích hợp hơn ZGC
java -XX:+UseSerialGC -Xmx512m SmallApp # Single thread GC, it overhead
java -XX:+UseG1GC -Xmx2g MediumApp # Balanced
java -XX:+UseZGC -Xmx16g LargeApp # ZGC value xuất hiện rõ
10. Tuning thực chiến — các flag quan trọng nhất
G1 production tuning:
java \
-XX:+UseG1GC \
-Xms16g -Xmx16g \ # Xms = Xmx cho server
-XX:MaxGCPauseMillis=100 \ # Soft pause target
-XX:InitiatingHeapOccupancyPercent=35 \ # Start concurrent mark som
-XX:G1HeapRegionSize=16m \ # Region size (auto hoac manual)
-XX:ConcGCThreads=4 \ # Concurrent GC threads
-XX:ParallelGCThreads=8 \ # STW GC threads
-Xlog:gc*=info:file=gc.log:time,uptime \ # GC logging mandatory
MyApp
ZGC production tuning:
java \
-XX:+UseZGC \
-Xms16g -Xmx16g \
-XX:ConcGCThreads=4 \ # Concurrent GC threads (default auto)
-XX:SoftMaxHeapSize=14g \ # Soft heap limit (ZGC specific)
-Xlog:gc*=info:file=gc.log \
MyApp
# ZGC it can tune hon G1 - heuristic tu thich nghi tot hon
Logging bắt buộc cho production:
-Xlog:gc*=info:file=/var/log/app/gc-%t.log:time,uptime,level:filecount=5,filesize=20m
# filecount=5: giu 5 file
# filesize=20m: rotate khi dat 20 MB
# Gui vao log aggregator (Datadog, ELK) de alert
11. Deep Dive
- JEP 248 — G1 as Default GC (Java 9) — openjdk.org/jeps/248 — rationale G1 làm default, so sánh với Parallel GC lúc đó.
- JEP 333 → 377 → 439 — ZGC evolution — openjdk.org/jeps/439 — JEP 439 (Java 21) Generational ZGC: thêm young/old generation, cải thiện throughput đáng kể. Đọc "Motivation" section để hiểu vì sao non-generational ZGC thiếu efficiency.
- JEP 189 — Shenandoah (Java 12) — openjdk.org/jeps/189 — thiết kế Brooks pointer và concurrent compaction. JEP 404 (Java 21 experimental) thêm generational.
- Per Liden "ZGC Internals" — InfoQ talk — infoq.com/presentations/zgc-internals/ — Per Liden (ZGC author) giải thích colored pointer, load barrier, forwarding table chi tiết với diagram. Bắt buộc xem nếu làm GC tuning.
- RedHat Shenandoah Whitepaper — rkennke.github.io/shenandoah-whitepaper/ — thiết kế chi tiết Brooks pointer, evacuation protocol, barrier elision optimization.
- HotSpot GC Tuning Guide (Java 21) — docs.oracle.com/en/java/javase/21/gctuning/ — official guide, có section riêng cho G1, ZGC, Parallel, Serial. Đọc section "Factors Affecting Garbage Collection Performance" trước khi tune.
- Aleksey Shipilёv — "JVM Anatomy Quarks" — shipilev.net/jvm/anatomy-quarks/ — chuỗi bài deep-dive, đặc biệt "Write Barriers" và "GC Design" quarks. Data-driven, benchmark-backed.
12. Self-check
Q1SATB (Snapshot-At-The-Beginning) là gì và tại sao G1 cần write barrier để maintain SATB?▸
SATB là thuật toán concurrent marking đảm bảo tính đúng đắn khi GC mark object đồng thời với app thread đang thay đổi reference graph.
Vấn đề không có SATB: nếu app thread xoá reference A→B trong lúc GC thread đã duyệt qua A nhưng chưa duyệt B, GC có thể bỏ sót B (đánh dấu là rác dù B vẫn live qua path khác).
SATB giải quyết bằng invariant: mọi object live tại thời điểm marking bắt đầu phải được mark live, kể cả nếu app thread xoá reference đến nó sau đó.
Write barrier thực hiện SATB: khi app thread ghi vào reference field (obj.ref = newValue), barrier ghi giá trị cũ (oldValue = obj.ref) vào SATB queue trước khi ghi. GC thread xử lý queue, mark oldValue live — đảm bảo object cũ không bị bỏ sót.
Chi phí:
- Mỗi reference write: ~5-10 ns để push vào SATB queue.
- Refinement thread chạy concurrent xử lý queue + update RSet.
- SATB queue drain cần short STW (Remark pause ~5-20ms).
SATB là write barrier — chỉ tốn khi ghi, không tốn khi đọc. Đây là lý do G1 phù hợp với read-heavy workload hơn write-heavy.
Q2Colored pointer trong ZGC là gì và tại sao nó cho phép concurrent compaction mà G1 không làm được?▸
Colored pointer: ZGC nhúng metadata vào các bit cao của 64-bit pointer (bit 41-45 trên Linux x86-64, không được OS dùng cho address). Các bit này encode trạng thái GC của object: đã mark, đã relocate, cần fixup.
Load barrier: mỗi lần app thread load reference, JIT chèn check metadata bit. Nếu bit báo pointer cũ (object đã relocate) → slow path: tra forwarding table, lấy địa chỉ mới, return địa chỉ mới cho app thread. Transparent với app code.
Tại sao G1 không concurrent compact:
- G1 dùng write barrier — tốn khi ghi, không check khi đọc.
- Khi GC muốn move object X, app thread có thể đang đọc reference đến X. Nếu GC đã copy X sang địa chỉ mới, app thread đọc địa chỉ cũ → stale data.
- Không có load barrier → không có mechanism intercept đọc cũ → không thể redirect sang địa chỉ mới → phải STW khi compact.
Tại sao ZGC concurrent compact:
- Load barrier intercept mọi reference read.
- GC copy object sang địa chỉ mới, ghi forwarding entry, set "relocated" bit trong pointer cũ.
- App thread đọc pointer cũ → load barrier thấy "relocated" bit → tra forwarding table → return địa chỉ mới. App thấy data đúng.
- Compaction concurrent hoàn toàn — không STW.
Trade-off: load barrier overhead ~5-10% throughput. Nhưng không STW cho compaction → pause dưới 2ms. Đánh đổi throughput lấy latency.
Q3Khác biệt giữa write barrier và read barrier là gì? Collector nào dùng loại nào và tại sao?▸
Write barrier: code được JIT chèn vào mỗi instruction ghi reference field (obj.field = x). Tốn chi phí khi ghi, không có overhead khi đọc.
Read barrier: code được JIT chèn vào mỗi instruction đọc reference field (x = obj.field). Tốn chi phí khi đọc, ít overhead khi ghi.
Collector và barrier type:
- G1: write barrier (SATB barrier + card table barrier). Tốn khi ghi, free khi đọc.
- ZGC: read barrier (load barrier check colored pointer). Tốn khi đọc.
- Shenandoah: read barrier (follow Brooks pointer). Tốn khi đọc.
- Parallel GC: minimal write barrier (card table only). Overhead nhỏ nhất.
Tại sao read barrier tốn overhead nhiều hơn trong practice: đọc reference xảy ra nhiều hơn ghi reference trong code thực tế (đặc biệt là traversal, lookup, business logic). Mỗi field access trong hot loop đều có barrier. Đây là lý do ZGC/Shenandoah overhead 10-15%, trong khi G1 overhead chỉ 5%.
Tại sao vẫn dùng read barrier: cho phép concurrent compaction. Write barrier không intercept đọc → không thể redirect stale pointer khi GC đang move object → phải STW khi compact. Read barrier intercept mọi load → có thể redirect sang địa chỉ mới → compact concurrent → pause dưới 10ms.
Tradeoff: G1 (write barrier) = throughput tốt hơn, pause dài hơn. ZGC (read barrier) = throughput thấp hơn 10-15%, pause dưới 2ms.
Q4G1 Full GC xảy ra khi nào và cách ngăn?▸
G1 fallback sang Full GC khi Mixed GC không dọn được Old gen đủ nhanh so với allocation rate. Cụ thể khi:
- Concurrent marking không kịp: alloc rate vượt marking throughput → Old gen đầy trước marking complete → không đủ thông tin để chọn region → Full GC.
- Humongous allocation fail: cần region liên tục cho object lớn nhưng heap fragmented → Full GC để compact.
- Evacuation failure: Young GC cần copy object sang Survivor/Old nhưng không có region free → Fail, trigger Full GC để giải phóng.
Diagnose: GC log có Pause Full (Ergonomics) hoặc Pause Full (Allocation Failure).
Ngăn Full GC G1:
- Start concurrent marking sớm hơn:
-XX:InitiatingHeapOccupancyPercent=35(default 45). Marking bắt đầu khi Old chiếm 35% thay vì 45% — nhiều thời gian hơn trước khi đầy. - Tăng GC thread:
-XX:ConcGCThreads=4— marking concurrent thread nhiều hơn. - Tăng heap reserve:
-XX:G1ReservePercent=15(default 10) — G1 giữ 15% heap làm buffer cho evacuation. - Giảm Humongous object: dùng DirectByteBuffer hoặc tăng region size.
- Tăng heap: nếu workload thực sự cần nhiều memory hơn.
- Switch ZGC: nếu pause target dưới 10ms và throughput overhead chấp nhận được.
Red flag: Full GC nhiều hơn 1 lần/giờ = heap sizing vấn đề hoặc allocation spike cần fix ở code level.
Q5Tại sao Generational ZGC (Java 21) cải thiện throughput so với non-generational ZGC?▸
Non-generational ZGC (Java 15-20): không phân biệt young/old. Mọi object trong cùng 1 space. GC phải scan toàn heap mỗi cycle để tìm dead object.
Vấn đề: generational hypothesis (phần lớn object chết trẻ) không được tận dụng. Short-lived object (temporary buffer, intermediate object) sống đến full GC cycle → GC phải copy nhiều object dù chúng sắp dead.
Hệ quả: ZGC pre-Java-21 tốn nhiều CPU cho copy object không cần thiết → throughput thấp hơn G1 đáng kể (~15-20%).
Generational ZGC (Java 21, JEP 439):
- Thêm Young generation (small region, GC thường xuyên) + Old generation.
- Short-lived object được collect trong Young GC cycle nhanh, không cần đợi full heap cycle.
- GC thread ít phải copy object vì Young GC catch phần lớn garbage sớm.
- Throughput overhead giảm từ ~20% xuống ~10% so với G1.
- Vẫn giữ pause dưới 10ms (colored pointer + load barrier).
Benchmark (JEP 439 data): workload mixed young/old object. Non-gen ZGC: 80% throughput so với G1. Gen ZGC: 90-95% throughput so với G1. Pause: cả 2 ZGC dưới 2ms.
Recommendation Java 21+: Generational ZGC là default choice cho latency-critical app. Non-generational ZGC vẫn available (-XX:-ZGenerational) nhưng không còn recommended.
Q6App cần SLA p99 dưới 10ms và heap 40 GB. Bạn chọn GC nào, version Java nào, và cấu hình thế nào?▸
Chọn: ZGC (Generational) trên Java 21+
Lý do:
- SLA p99 dưới 10ms: loại ngay Parallel GC (pause hàng giây) và G1 (pause 50-500ms cho heap 40 GB). Chỉ ZGC và Shenandoah đạt dưới 10ms.
- Heap 40 GB: vượt 32 GB → compressed OOP tắt (xem bài 07). G1 pause leo cao với heap lớn. ZGC designed cho heap TB.
- Java 21+: Generational ZGC throughput tốt hơn non-generational. Stable, production-ready.
Cấu hình:
java -XX:+UseZGC -Xms40g -Xmx40g -XX:SoftMaxHeapSize=36g # ZGC try to stay under 36g (buffer)
-XX:ConcGCThreads=6 # Concurrent GC, tune theo core count
-Xlog:gc*=info:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m -XX:+HeapDumpOnOutOfMemoryError MyAppKiểm tra sau switch:
- Benchmark throughput: so sánh RPS trước/sau. Expect giảm 10-15% — cần scale instance tương ứng.
- Monitor pause:
-Xlog:gc*=infoxem "Pause" events. Expect dưới 2ms thường. - Monitor TTSP (bài 09): nếu TTSP spike vẫn cao, check counted loop và JNI call.
- Memory: heap 40 GB + overhead ZGC ~20% = cần server ~48-50 GB RAM thực tế.
Nếu Shenandoah là option: tương đương ZGC về latency. Chọn ZGC trên OpenJDK (upstream), Shenandoah nếu cần RedHat support hoặc kiến trúc không support colored pointer.
Không switch trực tiếp production: rollout canary → measure → promote. GC change là high-risk config change.
Q7Humongous object trong G1 là gì và tại sao gây vấn đề?▸
Humongous object: object có kích thước vượt 50% G1 region size. Ví dụ region size 16 MB, object lớn hơn 8 MB là Humongous.
G1 allocate Humongous object vào 1 hoặc nhiều region liên tiếp dành riêng, gọi là Humongous region. Không có region nào khác share với Humongous object.
Vấn đề Humongous object gây ra:
- Không được GC trong Young GC: Young GC chỉ collect Eden + Survivor region. Humongous region được clean trong concurrent marking cycle (ít thường xuyên hơn). Humongous object thường live lâu hơn short-lived object → old gen pressure.
- Fragmentation: cần vùng region liên tiếp. Nếu heap fragmented (nhiều region nhỏ rải rác), không tìm được vùng liên tiếp đủ lớn → evacuation failure → Full GC.
- Memory waste: object 9 MB trong region 16 MB → 7 MB wasted per region. Với nhiều Humongous object → heap utilization giảm.
Phát hiện: GC log có Humongous regions: X->Y tăng nhanh. JFR event jdk.G1HeapSummary field humongousUsed.
Fix:
- Tăng region size:
-XX:G1HeapRegionSize=32m→ threshold Humongous tăng lên 16 MB. Object 9 MB không còn Humongous. - Dùng DirectByteBuffer (off-heap) cho buffer lớn.
- Pool và reuse buffer thay vì alloc/free mỗi request.
- Switch ZGC nếu Humongous object nhiều và không thể tránh — ZGC handle large object tốt hơn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên