Chapter 7. GarbageCollection
1. Garbage Collection là gì?
Garbage Collection là cơ chế tự động trong Java (và JVM - Java Virtual Machine) để quản lý bộ nhớ. Nó giải phóng bộ nhớ bằng cách thu hồi các đối tượng không còn được tham chiếu (unreachable objects) trong chương trình, giúp lập trình viên không phải quản lý bộ nhớ thủ công như trong C/C++.
Heap: Khu vực bộ nhớ chính nơi các đối tượng Java được lưu trữ. GC hoạt động chủ yếu trên Heap.
Đối tượng không tham chiếu: Một đối tượng không còn được tham chiếu trực tiếp hoặc gián tiếp từ các "root" (như stack, static variables, JNI references) sẽ được coi là "rác" và đủ điều kiện để thu hồi.
2. Cách Garbage Collection hoạt động
GC trong JVM hoạt động dựa trên một số nguyên tắc cơ bản:
a. Mark-and-Sweep
Đây là thuật toán cơ bản nhất:
Mark: GC duyệt từ các "GC Roots" (như biến local trong stack, biến static, thread) để đánh dấu các đối tượng đang được sử dụng (live objects).
Sweep: Các đối tượng không được đánh dấu sẽ bị xóa khỏi bộ nhớ.
Compact (tùy chọn): Một số GC di chuyển các đối tượng còn sống để giảm phân mảnh bộ nhớ.
b. Generational Hypothesis
JVM chia Heap thành các thế hệ (generations) dựa trên giả thuyết rằng:
Hầu hết các đối tượng chết ngay sau khi được tạo (short-lived).
Các đối tượng sống sót qua nhiều chu kỳ GC có xu hướng sống lâu hơn (long-lived).
Heap được chia thành:
Young Generation:
Eden: Nơi các đối tượng mới được tạo.
Survivor Spaces (S0, S1): Nơi các đối tượng sống sót sau Minor GC được chuyển tới.
Old Generation (Tenured): Chứa các đối tượng sống lâu dài.
Metaspace (từ Java 8 trở đi, thay thế PermGen): Lưu trữ metadata của class, không thuộc Heap nhưng vẫn được GC quản lý khi cần.
c. Minor GC và Major GC
Minor GC: Xảy ra ở Young Generation, nhanh và thường xuyên, thu hồi các đối tượng chết trong Eden và Survivor.
Major GC: Xảy ra ở Old Generation, chậm hơn vì phải quét toàn bộ Heap hoặc một phần lớn của nó.
Full GC: Quét và thu hồi trên toàn bộ Heap (Young + Old), thường gây "Stop-the-World" (ứng dụng tạm dừng).
3. Các thuật toán Garbage Collection trong JVM
JVM cung cấp nhiều thuật toán GC, mỗi cái phù hợp với các trường hợp sử dụng khác nhau. Dưới đây là các thuật toán phổ biến:
a. Serial GC
Dùng một luồng (single-threaded) để thực hiện GC.
Phù hợp với ứng dụng nhỏ, đơn giản hoặc máy có ít CPU.
Command:
-XX:+UseSerialGC
b. Parallel GC
Dùng nhiều luồng để thu gom rác, tối ưu cho throughput (lượng công việc hoàn thành trong thời gian nhất định).
Phù hợp với các ứng dụng cần xử lý nhanh, không quá quan tâm latency.
Command:
-XX:+UseParallelGC
c. CMS (Concurrent Mark-Sweep)
Tối ưu cho low-latency, giảm thời gian "Stop-the-World" bằng cách thực hiện một phần công việc đồng thời với ứng dụng.
Nhược điểm: Có thể gây phân mảnh bộ nhớ, không có compaction mặc định.
Command:
-XX:+UseConcMarkSweepGC
(bị deprecated từ Java 9, xóa bỏ từ Java 14).
d. G1 (Garbage-First)
Mục tiêu: Cân bằng giữa throughput và latency.
Chia Heap thành các region nhỏ, ưu tiên thu hồi vùng có nhiều rác nhất (garbage-first).
Phù hợp với ứng dụng lớn, Heap > 4GB.
Command:
-XX:+UseG1GC
(mặc định từ Java 9).
e. ZGC (Z Garbage Collector)
GC thế hệ mới, tập trung vào ultra-low latency (dừng ứng dụng dưới 10ms).
Dùng "colored pointers" để đánh dấu đối tượng mà không cần dừng ứng dụng lâu.
Phù hợp với ứng dụng cần latency cực thấp (real-time, trading systems).
Command:
-XX:+UseZGC
(thử nghiệm từ Java 11, ổn định từ Java 15).
f. Shenandoah
Tương tự ZGC, tối ưu low-latency, hoạt động đồng thời với ứng dụng.
Khác biệt: Không thuộc Oracle JDK, do Red Hat phát triển.
Command:
-XX:+UseShenandoahGC
.
4. Tối ưu hóa Garbage Collection
Là Senior Java Developer, bạn cần biết cách tinh chỉnh GC để phù hợp với ứng dụng:
a. Cấu hình kích thước Heap
-Xms
: Kích thước Heap ban đầu.-Xmx
: Kích thước Heap tối đa.Ví dụ:
-Xms512m -Xmx2048m
(Heap bắt đầu từ 512MB, tối đa 2GB).Quy tắc: Đặt
-Xms
=-Xmx
để tránh việc JVM resize Heap liên tục.
b. Điều chỉnh kích thước Young/Old Generation
-XX:NewRatio
: Tỷ lệ giữa Young và Old (mặc định 2, tức Young = 1/3 Heap).-XX:NewSize
và-XX:MaxNewSize
: Đặt kích thước cụ thể cho Young Generation.Ví dụ:
-XX:NewRatio=3
(Young = 1/4 Heap).
c. Theo dõi và phân tích GC
GC Logging: Bật log để phân tích hiệu suất GC:
-Xlog:gc
(Java 9+).-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
(Java 8 trở về trước).
Công cụ:
VisualVM: Theo dõi Heap, GC activities.
GCViewer: Phân tích log GC.
JFR (Java Flight Recorder): Ghi lại chi tiết runtime, bao gồm GC.
d. Giảm áp lực lên GC
Tránh tạo quá nhiều đối tượng tạm thời (temporary objects) không cần thiết.
Sử dụng
StringBuilder
thay vì nối chuỗi bằng+
.Giải phóng tham chiếu thủ công khi cần (gán
null
cho đối tượng lớn không còn dùng).
5. Một số mẹo thực tế từ Senior Java
Hiểu workload của ứng dụng:
Nếu cần throughput cao (batch processing): Dùng Parallel GC.
Nếu cần latency thấp (web app, API): Dùng G1 hoặc ZGC.
Đừng tối ưu hóa quá sớm: Chỉ can thiệp khi có bằng chứng (log, metric) cho thấy GC là bottleneck.
Full GC là kẻ thù: Nếu Full GC xảy ra thường xuyên, kiểm tra leak memory hoặc tăng kích thước Heap.
Memory Leak: GC không giải quyết được nếu bạn vô tình giữ tham chiếu đến đối tượng không cần thiết (ví dụ: trong
HashMap
hoặcstatic
field).
6. Ví dụ thực tế
Giả sử bạn có một ứng dụng web bị chậm do GC:
Bật GC log:
-Xlog:gc:/path/to/gc.log
.Phân tích log thấy Full GC xảy ra mỗi 5 phút, mỗi lần dừng 2 giây.
Kiểm tra Heap:
-Xmx
chỉ 1GB, nhưng ứng dụng cần xử lý 100k request/phút.Tăng
-Xmx
lên 4GB, bật G1 GC:-XX:+UseG1GC
.Kết quả: Full GC giảm xuống 1 lần/giờ, latency cải thiện.
7. Kết luận
Garbage Collection là một phần quan trọng trong JVM, và hiểu sâu về nó giúp bạn tối ưu hóa hiệu suất ứng dụng. Từ việc chọn thuật toán phù hợp, cấu hình Heap, đến phân tích log, bạn có thể kiểm soát GC thay vì để nó kiểm soát bạn.
Last updated