Garbage Collection — G1, ZGC, Parallel và cách chọn
GC làm gì, tại sao Java không cần free thủ công. Generational hypothesis, mark-sweep-compact, copying. 3 collector chính: Parallel (throughput), G1 (default modern), ZGC (low-latency sub-ms). Khi nào chọn cái nào, đọc GC log thế nào.
GC là feature lớn nhất phân biệt Java với C/C++. Bạn không bao giờ gọi free(obj) — JVM tự reclaim memory khi object không còn reference. Đổi lại, JVM phải scan heap thường xuyên để biết cái nào còn dùng, cái nào dead.
Cost không miễn phí: trong service throughput thấp, GC pause vài chục ms thi thoảng vô hại. Trong service latency-sensitive (trading platform 1ms p99, real-time bidding 50ms p99), GC pause 200ms có thể trigger SLA breach. Lựa chọn collector sai → user thấy lag, alert nổ.
JVM cung cấp 3 collector chính (Java 21):
- Parallel GC: tối ưu throughput, pause dài chấp nhận được. Default Java 8 server.
- G1 GC: balanced, default Java 9+ server. Pause ~50-200ms.
- ZGC: low-latency, pause dưới 10ms (thường dưới 1ms), heap đến 16TB.
Bài này đi qua: generational hypothesis (vì sao chia young/old), 3 algorithm cốt lõi (mark-sweep, copying, compacting), chi tiết 3 collector (cách hoạt động, pause profile, khi dùng), đọc GC log, và pattern chọn collector theo workload.
1. Analogy — Dọn nhà 3 cách
Bạn có căn nhà chứa đồ đạc — vật cũ vứt đi, vật mới giữ.
Parallel GC = thuê 8 người dọn cùng lúc: nhanh hoàn thành (throughput cao), nhưng cả nhà đóng cửa 30 phút lúc dọn, không ai vào ra được. Phù hợp dọn theo lịch (đêm khuya).
G1 = chia nhà thành 50 phòng nhỏ, dọn xoay vòng từng phòng: mỗi lần đóng 1 phòng 5 phút, ai đi qua đi lại vẫn dùng phòng khác. Tổng dọn lâu hơn, nhưng "không bao giờ đóng cả nhà". Phù hợp nhà đông người ra vào.
ZGC = mỗi món đồ có nhãn màu, mọi việc dọn diễn ra đồng thời với người đang ở: thậm chí khi 1 nhân viên đang nhặt món lên kiểm, người khác có thể tiếp tục dùng tay. Không bao giờ "đóng phòng". Phức tạp nhưng latency cực thấp.
| Đời thường | GC |
|---|---|
| Dọn nhà | Garbage collection |
| Vật cũ vứt | Object unreachable |
| Đóng cửa nhà | Stop-the-world pause |
| 8 nhân viên dọn cùng | Parallel GC |
| Chia phòng dọn xoay vòng | G1 GC |
| Dán nhãn màu, dọn concurrent | ZGC |
3 collector × 2 trục: Parallel (throughput cao, pause dài), G1 (balanced default), ZGC (latency cực thấp). Chọn dựa trên: SLA p99 latency app cần là bao nhiêu? > 200ms → Parallel; 50-200ms → G1; <10ms → ZGC.
2. Reachability — định nghĩa "rác"
Object là rác khi không có reference nào dẫn từ GC root đến nó.
GC root:
- Static field của loaded class.
- Local variable trên stack mọi thread.
- JNI reference.
- Active thread object.
Algorithm "reachability": từ root, traverse graph reference. Object visit được = live. Không visit = dead, có thể reclaim.
Stack/Static (GC root)
|
v
[A] -> [B] -> [C]
\
[D] -> [E]
[F] -> [G] (khong tu root toi -> rac, du F va G ref nhau)
Không phải reference count (như Python) — Java dùng reachability để xử lý cycle. F ↔ G ref lẫn nhau (refcount > 0) nhưng không reachable từ root → vẫn là rác. Reference counting fail vì cycle.
3. 3 algorithm cốt lõi
Mark-Sweep
flowchart LR
A[1. Mark<br/>Traverse from root<br/>mark live object] --> B[2. Sweep<br/>Iterate heap<br/>free unmarked]Mark: BFS/DFS từ root, mark mỗi object visit-được. Sweep: scan toàn heap linear, free object không mark.
Đơn giản. Vấn đề: fragmentation — object dead nằm rải rác → free space rời rạc → không alloc được object lớn dù tổng free đủ.
Copying (Cheney's algorithm)
flowchart LR
A[Heap chia 2 nua: From + To] --> B[Copy live tu From sang To]
B --> C[From cleared - rac di theo]
C --> D[Swap From/To role]Heap chia 2 nửa From + To. Alloc trong From. GC: copy mọi live object từ From → To, gom contiguous. From clear hoàn toàn (không cần sweep từng object — clear cả region 1 lần). Swap role.
Ưu: no fragmentation, allocation nhanh (bump pointer trong To). Nhược: dùng nửa heap — waste space. Phù hợp young gen (object chết nhiều, copy ít).
Mark-Sweep-Compact
flowchart LR
A[1. Mark] --> B[2. Compact<br/>shift live object<br/>about beginning of region]Mark như mark-sweep. Sau đó compact: shift mọi live object về đầu region → free space contiguous.
Ưu: no fragmentation. Nhược: cost shift object (cập nhật mọi reference đến object đã shift).
Old gen thường dùng mark-sweep-compact — object già không di chuyển nhiều, compact ít thường xuyên.
4. Generational hypothesis — chia young/old
Profile thực tế: 90-95% object chết trẻ (alloc xong dùng vài microsecond rồi không reference). 5-10% sống lâu (cache, singleton, long-lived state).
Tách heap thành 2 region với strategy khác:
- Young: GC thường xuyên (vài giây), copy algorithm (rẻ vì ít object live).
- Old: GC ít thường xuyên (vài phút), mark-sweep-compact.
Heap layout:
+-----------+-----------------------------+
| Young | Old |
| (30%) | (70%) |
+-----------+-----------------------------+
| Eden | S0 | S1 | Tenured |
+-----------+-----------------------------+
Minor GC vs Major (Full) GC
Minor GC: scan young, copy live sang Survivor, promote object già sang Old. Nhanh (vài ms cho young 1GB) — vì 90% Eden là rác, copy ít.
Major / Full GC: scan toàn heap (young + old). Chậm hơn nhiều (hàng trăm ms cho heap 10GB). Trigger khi:
- Old đầy (object promote từ young không có chỗ).
System.gc()(don't call).- Metaspace đầy (full GC để unload class loader).
Production tip: monitor major GC frequency. > 1 major GC/giờ là red flag — heap quá nhỏ hoặc leak.
Tenuring
Object survive bao nhiêu round minor GC trước khi promote sang Old? Default ~15 (-XX:MaxTenuringThreshold=15).
Object age tăng mỗi minor GC. Đạt threshold → next GC promote sang Old.
Adaptive: nếu Survivor đầy → JVM hạ threshold → promote sớm để giải phóng young. -XX:+PrintTenuringDistribution show distribution thực tế.
5. Parallel GC
Default Java 8 server, deprecated dần Java 9+.
Cơ chế: nhiều thread (thường = #core) làm GC parallel. Algorithm:
- Young: copying (Eden + 2 Survivor).
- Old: mark-sweep-compact.
- Stop-the-world trong toàn bộ GC — app pause.
Pause time:
- Minor GC: vài chục ms (1GB young).
- Full GC: vài trăm ms đến giây (10GB heap) → vài giây (50GB heap).
Ưu:
- Throughput cao nhất: ít overhead, scale theo CPU.
- Đơn giản, predictable.
Nhược:
- Pause dài, không phù hợp latency-sensitive.
- Heap > 10GB pause time leo cao.
java -XX:+UseParallelGC -Xmx4g MyApp
Khi dùng:
- Batch job, ETL, scientific compute — không user request, throughput là tất cả.
- Heap nhỏ (< 4GB), latency không quan trọng.
6. G1 GC
Default Java 9+ server. Mainstream production choice 2017-nay.
Cơ chế: chia heap thành ~2048 region đều nhau (1MB-32MB tùy heap size). Mỗi region label là Eden, Survivor, Old, hoặc Humongous (object > 50% region size). Region không cố định — có thể đổi role qua time.
+------+------+------+------+------+------+------+
| Eden | Old | Old | Eden | Surv | Old | Eden |
+------+------+------+------+------+------+------+
| Eden | Hum | Old | Old | Eden | Surv | Old |
+------+------+------+------+------+------+------+
Cycle G1
- Young GC: scan Eden + Survivor regions. Stop-the-world. Copy live sang Survivor / Old. Pause ~10-50ms.
- Concurrent marking: chạy đồng thời với app thread, mark live object trong Old. Không pause.
- Mixed GC: Young + một số Old region được "best to collect" (chọn region có nhiều rác nhất → "garbage first"). Pause ~50-200ms.
- Full GC (last resort): nếu mixed GC không kịp → fall back full GC. Pause hàng giây — bug.
Pause goal
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx8g MyApp
-XX:MaxGCPauseMillis=200 (default): G1 cố giữ pause ≤ 200ms. Soft target — G1 dùng heuristic chọn số region scan mỗi cycle để fit. Không guarantee absolute.
Set 50-100ms cho web service. Set 500ms-1s cho throughput-oriented.
Khi dùng G1
- Heap 4-32GB.
- Web service, microservice — pause 50-200ms acceptable.
- App có mixed workload (some throughput, some latency).
- Default cho hầu hết production app.
Tune G1
-XX:G1HeapRegionSize=16m— region size, default tự compute.-XX:InitiatingHeapOccupancyPercent=45— start concurrent marking khi old đạt 45% (default). Set thấp nếu thấy mixed GC không kịp.-XX:G1MixedGCCountTarget=8— số mixed GC cycle để dọn Old (default 8).
Tune G1 phức tạp — đo trước, tune sau. Default thường đủ.
7. ZGC — low-latency
Production-ready Java 15+. Default Java 21+ cho heap lớn.
Mục tiêu: pause dưới 10ms, thường dưới 1ms, không scale theo heap size. Heap 16TB pause vẫn dưới 10ms.
Cơ chế cốt lõi: colored pointer + load barrier
ZGC dùng colored pointer — bit metadata trong reference (con trỏ object).
64-bit reference layout (Linux):
[unused: 17 bit][marked0: 1][marked1: 1][remapped: 1][finalizable: 1][address: 44 bit]
4 bit metadata trong pointer biểu thị state object trong GC cycle (đã mark? đã relocate?).
Load barrier: mỗi lần đọc reference, JVM check color bit. Nếu state cần update (vd object đã relocate, pointer cũ trỏ chỗ cũ) → JVM trigger fixup trong load barrier — update pointer atomic, rồi return.
Hệ quả: GC có thể relocate object đồng thời với app đang chạy. App đọc pointer cũ → load barrier silently update → app thấy pointer mới. Không cần stop-the-world.
Phase ZGC
flowchart LR
A[Pause: Mark Start] --> B[Concurrent Mark]
B --> C[Pause: Mark End]
C --> D[Concurrent Reference Processing]
D --> E[Concurrent Relocate]
E --> F[End cycle]Pause chỉ ở 2 điểm: mark start (~1ms) và mark end (~1ms). Mọi work thực (mark, relocate) concurrent với app.
Trade-off
Ưu:
- Pause cực ngắn: < 10ms thường < 1ms, không phụ thuộc heap size. Heap 1TB pause = pause heap 1GB.
- Heap lớn: hỗ trợ đến 16TB.
- Ít cần tune: heuristic tự thích nghi.
Nhược:
- Throughput thấp hơn G1 ~5-10%: load barrier check mọi reference đọc, overhead.
- CPU usage cao hơn: GC concurrent thread chạy song song với app, ăn CPU.
- Memory overhead 5-10%: cần extra space cho relocate.
java -XX:+UseZGC -Xmx16g MyApp
Java 21+: ZGC mặc định generational (-XX:+ZGenerational) — kết hợp benefit generational vào ZGC. Phiên bản cũ ZGC chỉ có "old" — fail short-lived garbage hiệu quả hơn G1 dù pause ngắn.
Khi dùng ZGC
- SLA latency p99 < 10ms — trading platform, real-time bidding, gaming server.
- Heap rất lớn (> 32GB) — pause G1 leo cao.
- Long-running service — throughput thấp 5% chấp nhận đổi pause ngắn.
Không dùng:
- Batch / throughput-oriented — Parallel hoặc G1 throughput cao hơn.
- Heap nhỏ (< 4GB) + đơn giản — overhead không bù lại.
8. Đọc GC log
java -Xlog:gc*=info:file=gc.log:time,uptime,level,tags MyApp
Format Java 9+ unified log. Output mẫu (G1):
[2026-04-27T10:00:00.123+0700][1.234s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[2026-04-27T10:00:00.145+0700][1.256s][info][gc,heap ] GC(0) Eden regions: 24->0(25)
[2026-04-27T10:00:00.145+0700][1.256s][info][gc,heap ] GC(0) Survivor regions: 0->3(3)
[2026-04-27T10:00:00.145+0700][1.256s][info][gc,heap ] GC(0) Old regions: 0->0
[2026-04-27T10:00:00.145+0700][1.256s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 192M->24M(256M) 22.345ms
Đọc:
GC(0): ID cycle.Pause Young (Normal): minor GC (young).Eden regions: 24->0(25): trước GC 24 region, sau 0 (clean), max 25.192M->24M(256M): heap before → after (total). Reclaim 168MB.22.345ms: pause duration.
Pattern cần để ý
- Throughput: % time app run vs GC. Tốt > 95%.
- Pause time p99: cao nhất 99% events. Match SLA.
- Frequency: minor GC mỗi 5-30s OK, mỗi giây = pressure cao.
- Promote rate: object promoted Old per second. Cao = old fill nhanh = full GC sớm.
- Allocation rate: byte alloc / second. Tăng → pressure tăng.
Tool phân tích
- GCEasy (web): paste log → report tự động.
- GCViewer (desktop): visualize timeline.
- JFR: capture GC event với detail (cause, region count, allocation rate).
9. Cách chọn collector
| Workload | Collector |
|---|---|
| Batch job, ETL | Parallel GC — throughput max |
| Web/microservice typical (heap 4-32GB) | G1 GC — default tốt |
| Latency-sensitive < 10ms p99 | ZGC |
| Heap > 32GB | ZGC |
| Heap < 2GB, đơn giản | Serial GC (single thread) — overhead thấp |
| Container 256MB | Serial hoặc G1 nhỏ |
Default Java 21: G1. Đổi qua flag -XX:+UseZGC, -XX:+UseParallelGC, -XX:+UseSerialGC.
Flowchart quyết định
flowchart TD
A[App workload] --> B{Latency SLA?}
B -->|p99 < 10ms| C[ZGC]
B -->|p99 50-200ms| D{Heap size?}
B -->|p99 > 500ms| E[Parallel GC]
D -->|< 32GB| F[G1 GC default]
D -->|>= 32GB| CCommon mistakes
- Set
-Xmxquá thấp: GC chạy liên tục, "GC overhead limit exceeded". -Xms != -Xmx: server long-running nên equal — predictable, no resize.- Tune nhiều flag không đo: default thường tốt, đo trước khi tune.
- Ignore pause time: dùng Parallel cho web service → user complaint lag intermittent.
- Dùng ZGC cho heap nhỏ: overhead không bù được, dùng G1.
10. Pitfall tổng hợp
❌ Nhầm 1: Gọi System.gc() để "force clean".
System.gc(); // Hint - JVM co the ignore
// Voi G1: trigger full GC -> pause dai
✅ Đừng gọi. Tin GC heuristic. Disable bằng -XX:+DisableExplicitGC.
❌ Nhầm 2: Tin "GC tuning sẽ fix mọi performance".
Allocation rate cao -> tune GC khong giai quyet, refactor giam alloc.
✅ Profile alloc trước. Cắt alloc thừa hiệu quả hơn tune GC.
❌ Nhầm 3: Heap khổng lồ "an toàn".
-Xmx100g # Pause Parallel/G1 leo cao
✅ Heap lớn dùng ZGC, hoặc shard logic vào nhiều JVM nhỏ.
❌ Nhầm 4: Catch GC log không monitor.
-Xlog:gc* -> log file 100MB/ngay khong ai doc
✅ Ship log vào tool (Datadog, New Relic), set alert pause > N ms.
❌ Nhầm 5: Long-lived weak/soft reference cache không design.
Map<K, SoftReference<V>> cache = ...;
// SoftRef chi free khi sap OOM -> heap luon gan full -> GC chay lien tuc
✅ Dùng Caffeine/Guava cache với explicit eviction.
❌ Nhầm 6: Container không pass JVM flag.
docker run myapp java MyApp # Default JVM detect 256MB, dung Serial GC
✅ Container Java 11+ tự nhận cgroup limit. Java 8 cần -XX:+UseContainerSupport. Set -Xmx explicit để control.
11. 📚 Deep Dive Oracle
Spec / reference chính thức:
- HotSpot GC Tuning Guide (Java 21) — official tuning manual, recommend đọc full.
- JEP 333: ZGC Experimental + JEP 377: ZGC Production + JEP 439: Generational ZGC — tiến hoá ZGC.
- JEP 248: G1 default — Java 9 đổi default sang G1.
- JEP 379: Shenandoah Production — alternative low-latency GC (Red Hat).
- Shipilev's Blog — Aleksey Shipilev (HotSpot perf engineer) viết về GC, JIT, Unsafe, JMH.
- GCEasy — web tool phân tích GC log.
- JITWatch — visualize GC + JIT log.
Ghi chú: HotSpot GC Tuning Guide là tài liệu authoritative, dày 100+ trang nhưng có cấu trúc tốt — đọc theo collector bạn dùng. JEP 439 (Generational ZGC) Java 21 là milestone — ZGC giờ generational nên hiệu quả với short-lived garbage tương đương G1, đồng thời giữ pause <1ms. Aleksey Shipilev's blog là kho rare knowledge: ngoài GC tuning, có pattern code Java idiomatic vs allocation, lock optimization, escape analysis case study. Production rule of thumb: dùng default G1 trên Java 21, đo p99 latency, switch ZGC nếu cần <10ms.
12. Tóm tắt
- GC reclaim object unreachable từ GC root (static field, stack local, JNI ref, active thread). Reachability không phải refcount — handle cycle.
- 3 algorithm cốt lõi: mark-sweep (đơn giản, fragmentation), copying (no frag, dùng nửa heap), mark-sweep-compact (no frag, cost shift).
- Generational hypothesis: 90-95% object chết trẻ → tách young (copying, GC thường) + old (mark-compact, GC ít).
- Minor GC: scan young, fast (vài ms). Major / Full GC: scan toàn heap, chậm.
- Tenuring: object survive ~15 minor GC → promote Old. Adaptive nếu Survivor đầy.
- Parallel GC: throughput max, pause dài, batch / throughput workload. Default Java 8.
- G1 GC: chia heap thành region, "garbage first" — chọn region nhiều rác. Default Java 9+, pause 50-200ms typical.
- ZGC: colored pointer + load barrier, GC concurrent. Pause <10ms thường <1ms, không scale heap. Java 15+ production. Java 21+ generational.
- Chọn collector theo SLA latency: > 200ms → Parallel; 50-200ms → G1; <10ms → ZGC.
- Heap > 32GB: nên ZGC (G1 pause leo cao).
-Xms = -Xmxcho server long-running.-XX:MaxGCPauseMillis=200G1 soft target.- Đọc GC log với
-Xlog:gc*=info:file=gc.log. Tool: GCEasy, GCViewer, JFR. - KPI monitor: pause time p99, throughput %, frequency, promote rate, allocation rate.
- Tránh:
System.gc(), heap khổng lồ với Parallel/G1, soft/weak reference cache không bound. - Container: Java 11+ tự nhận cgroup. Vẫn nên set
-Xmxexplicit cho predictable.
13. Tự kiểm tra
Q1Vì sao Java dùng reachability thay vì reference counting như Python?▸
Reference counting: mỗi object có counter, increment khi có ref mới, decrement khi mất ref. Counter = 0 → free.
Vấn đề chính: cycle. 2 object ref lẫn nhau → counter cả 2 luôn ≥ 1, dù không ai bên ngoài ref chúng → leak vĩnh viễn.
class Node { Node next; }
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null; b = null;
// Refcount a, b van la 1 (do tu ref nhau) -> leak voi refcount
// Reachability: tu root khong toi a, b -> rac, freePython solution: refcount + cycle detector chạy thi thoảng. Phức tạp, vẫn có overhead.
Java reachability: BFS/DFS từ GC root (static field, stack local, JNI ref, active thread). Object visit = live, không visit = rác. Cycle tự động xử lý — group cycle nếu không reachable từ root sẽ bị reclaim.
Trade-off:
- Refcount: deterministic free (counter giảm 0 → free ngay). Không pause. Nhưng cycle leak, mỗi assignment có overhead inc/dec.
- Reachability: handle cycle. Allocation cực rẻ (no inc). Nhưng cần pause để scan, latency variable.
Java chọn reachability vì: (1) cycle phổ biến trong OOP code (parent-child, observer, graph data); (2) JIT + bump pointer alloc cực nhanh khi không track refcount; (3) modern GC (G1, ZGC) pause đã rất ngắn.
Swift dùng refcount (ARC) vì target mobile, deterministic timing quan trọng cho UI. Java target server, throughput dominant.
Q2Generational hypothesis là gì, và vì sao nó là foundation cho GC modern?▸
Quan sát empirical từ 1980s+: đa số object chết trẻ. Profile thực tế:
- 90-95% object alloc xong dùng vài microsecond rồi không reference nữa.
- Vd: intermediate object trong stream pipeline, local trong method, parameter wrapper, exception object debug.
- 5-10% sống lâu: cache, singleton, long-lived state.
Nếu GC không tận dụng pattern này → scan toàn heap mỗi cycle → chậm tuyến tính theo heap size.
Generational design:
- Tách heap thành Young + Old.
- Alloc luôn vào Young (Eden) — mọi object mới ở đây.
- GC Young thường xuyên (vài giây 1 lần) — vì 90% Eden đã chết → scan ít, copy ít.
- Object survive nhiều round Young → promote Old.
- GC Old ít thường xuyên (vài phút) — vì object già khả năng cao vẫn live.
Hiệu ứng: GC cycle phổ biến (Young) chỉ scan 30% heap với 10% live → fast. Full GC (toàn heap) hiếm.
Algorithm phù hợp:
- Young: copying GC. Hiệu quả khi ít object live (chỉ copy ít). Free space contiguous, alloc nhanh.
- Old: mark-sweep-compact. Hiệu quả khi nhiều object live (copy đắt).
Foundation: G1, ZGC (Java 21), Shenandoah, Parallel — tất cả generational. Exception: ZGC pre-Java 21 không generational, fix với JEP 439.
Edge case: workload "anti-generational" — ít alloc nhưng object lớn long-lived (vd in-memory cache 100GB) — generational benefit ít, full GC tỷ lệ cao hơn. ZGC giữ pause ngắn nhờ concurrent algorithm thay vì rely generational.
Q3Khác biệt thực tế giữa G1 và ZGC — vì sao G1 không thay được ZGC cho latency-sensitive app?▸
G1: heap chia ~2048 region. Mỗi GC cycle chọn subset region scan. Stop-the-world trong khi scan.
Pause time = thời gian scan region được chọn. Để pause 50ms, G1 chỉ scan ~50MB worth region (tùy alloc rate). Heap lớn → cần scan nhiều region để giải phóng đủ → pause dài lên dần. Heap 100GB pause có thể vượt 1s.
ZGC: dùng colored pointer + load barrier. GC mark + relocate object concurrent với app thread. App đọc pointer → load barrier check color bit, nếu cần update → fix tại chỗ.
Pause ZGC chỉ ở 2 điểm tổng cộng <2ms cho mọi heap size:
- Mark Start ~1ms — scan root.
- Mark End ~1ms — finalize mark.
Mọi work thực (mark traverse heap, relocate object) chạy concurrent — không pause.
Trade-off:
- ZGC throughput thấp hơn G1 ~5-10%: load barrier check mỗi reference đọc → overhead ~5%.
- ZGC CPU cao hơn: GC thread chạy concurrent ăn 1-2 core thường xuyên, không chỉ lúc GC.
- ZGC memory overhead 5-10%: cần extra space cho relocate (read pointer cũ, copy sang chỗ mới).
Khi G1 đủ dùng:
- SLA p99 latency 50-200ms.
- Heap < 32GB.
- Throughput quan trọng (web, microservice).
Khi cần ZGC:
- SLA p99 latency < 10ms (trading, real-time bidding, gaming).
- Heap > 32GB (G1 pause leo cao).
- Long-running service, throughput thấp 5% chấp nhận đổi pause cực ngắn.
Production trend Java 21+: ZGC generational thu hẹp gap throughput với G1. Trong 2 năm tới có thể thay G1 làm default cho server long-running.
Q4Tại sao System.gc() được coi là anti-pattern?▸
System.gc() được coi là anti-pattern?3 lý do:
- Trigger Full GC:
System.gc()mặc định request toàn heap GC (young + old). Pause dài hơn nhiều minor GC. Trong G1: pause hàng trăm ms; Parallel: pause hàng giây với heap lớn. - Phá pattern adaptive của GC: GC modern (G1, ZGC) tune timing dựa trên alloc rate, promote rate, heuristic.
System.gc()trigger ngoài lịch → ngắt cycle, GC phải re-baseline metric. - Không guarantee gì: spec nói "JVM should make best effort" — JVM có thể ignore.
-XX:+DisableExplicitGCtắt hẳn.
Thường gọi System.gc() với 2 motivation sai:
- "Free memory ngay sau xong batch": GC eventually free khi cần. Force trigger không giúp app — chỉ chậm thêm pause.
- "Get accurate memory measurement": vd benchmark muốn loại GC noise. Đúng hơn dùng JMH (handle GC quiescing automatic) hoặc
jcmd VM.gcngoài app.
Exception hợp lý (rất hiếm):
- DirectByteBuffer cleanup: Cleaner phụ thuộc GC chạy → call
System.gc()để force free direct memory. Pattern này có trong Netty / NIO code khi cần guaranteed cleanup. - Heap dump trước test: muốn dump không có rác → force GC trước (qua
jcmd, không từ app code).
Production rule: nếu thấy System.gc() trong code, audit lý do, hầu hết case xóa được. Nếu thực sự cần (DirectByteBuffer leak, bug 3rd-party lib), document rõ.
Disable global: -XX:+DisableExplicitGC ép mọi System.gc() thành no-op. An toàn cho production server không control 100% code (3rd-party lib có thể call).
Q5Đoạn GC log sau cho biết gì về sức khoẻ app? 192M->24M(256M) 22.345ms▸
192M->24M(256M) 22.345msFormat: before -> after (total) duration.
- 192M before: heap dùng 192MB trước GC.
- 24M after: heap dùng 24MB sau GC. Reclaim 168MB.
- 256M total: heap size hiện tại 256MB.
- 22.345ms duration: pause time.
Phân tích:
- Reclaim ratio: 168/192 = 87%. Tốt — đa số object là rác (generational hypothesis hold).
- Heap utilization sau GC: 24/256 = 9%. Heap dư thoải mái, không pressure.
- Pause 22ms: OK cho web service (SLA 50-200ms p99).
Cần thêm context để evaluate full:
- Frequency: 22ms mỗi 10 giây OK; mỗi 1 giây = pressure cao, 2-3% time GC.
- Type: minor (young) vs major (full)? Minor 22ms tốt; major 22ms cực tốt (heap nhỏ).
- Trend: heap-after có tăng dần qua time? → potential leak. Heap-after stable = healthy.
Red flag pattern:
GC 1: 192M->180M(256M) 50ms
GC 2: 250M->240M(256M) 80ms
GC 3: 256M->255M(256M) 200ms
GC 4: 256M->256M(256M) 500ms (Full GC)
OutOfMemoryErrorAfter không giảm = leak. Reclaim ratio thấp dần = object già không chết = old gen growing → Full GC → OOM.
Healthy pattern:
GC 1: 192M->24M(256M) 20ms
GC 2: 195M->26M(256M) 22ms
GC 3: 198M->25M(256M) 21ms
...After stable, reclaim ratio > 80%, pause stable. App workload steady, no leak.
Tool: GCEasy upload log → tự generate report với metric này, plus pause distribution histogram.
Q6Vì sao container Docker hay gây vấn đề với JVM GC, và làm sao fix?▸
Vấn đề lịch sử (Java 8 trước Update 191):
- JVM detect memory + CPU từ
/proc/meminfovà/proc/cpuinfo— phản ánh host machine, không phải cgroup container limit. - Container 256MB trên host 64GB → JVM thấy 64GB → set heap default 16GB → ăn vượt 256MB cgroup → kernel OOM kill (silent, no Java exception).
- JVM dùng GC Parallel với #core = 32 (host) → 32 GC thread cho container giới hạn 1 core → context switch chí mạng.
Java 8u191+ và Java 11+ fix: JVM nhận cgroup limit (+UseContainerSupport default on). Nhưng:
- Default heap = 25% container memory (max RAM percentage). Container 1GB → heap 256MB. Có thể quá nhỏ với app real.
- GC thread tự thích nghi #core container.
Best practice production:
# Set heap explicit (60-70% container memory)
JAVA_OPTS="-Xms2g -Xmx2g"
# Cho container 4GB:
docker run -m 4g -e JAVA_OPTS myapp java $JAVA_OPTS MyApp
# Hoac qua flag JVM:
java -XX:MaxRAMPercentage=70 MyApp # Heap 70% container RAMLý do giữ buffer 30%: heap không phải toàn bộ memory. Cần chỗ cho:
- Metaspace (~256MB).
- Direct memory (DirectByteBuffer, NIO).
- Code cache (~240MB).
- Thread stack (1MB × thread count).
- Native memory JNI / GC internal.
Tổng các region vượt container → OOM Kill silent.
Monitoring container:
docker statsshow memory usage realtime.- Cảnh báo > 90% container memory → tăng container hoặc giảm heap.
- JFR capture allocation rate, GC pause để correlate với container OOM event.
Kubernetes: resources.limits.memory: 4Gi phải khớp với container. Set requests = limits để pod predictable scheduling.
Java 21 thêm sensor cgroup v2 chuẩn → behavior consistent. Vẫn cần explicit -Xmx cho production reliable.
Bài tiếp theo: JVM tools — jstack, jmap, jstat, JFR
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...