CodeGym /Các khóa học /JAVA 25 SELF /Bộ gom rác: G1, ZGC, Shenandoah, so sánh

Bộ gom rác: G1, ZGC, Shenandoah, so sánh

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu về bộ gom rác (GC)

Nếu bạn từng lập trình bằng C hoặc C++, hẳn bạn đã phải tự giải phóng bộ nhớ bằng free() hoặc delete. Trong Java, mọi thứ đơn giản hơn nhiều: bạn tạo đối tượng bằng new, và không cần tự xóa nó — đã có “người quét dọn” chuyên trách gọi là bộ gom rác (Garbage Collector, GC).

GC là một phần của JVM tự động giải phóng bộ nhớ do các đối tượng không còn tham chiếu chiếm dụng. Nhờ đó, lập trình viên Java không phải lo quên giải phóng bộ nhớ (và bị rò rỉ), hoặc ngược lại, vô tình xóa một đối tượng vẫn còn cần (và gây crash).

Nhưng, giống như bất kỳ “người quét dọn” nào, GC không hoàn hảo: đôi khi nó có thể can thiệp vào đúng lúc không phù hợp, thực hiện “tổng vệ sinh” (Stop-the-World), hoặc hoạt động không nhanh như mong muốn. Vì vậy trong JVM tồn tại nhiều triển khai GC khác nhau — và chọn đúng loại có thể ảnh hưởng đáng kể đến hiệu năng ứng dụng.

Các loại bộ gom rác chính

Serial GC

  • Serial GC — bộ gom rác đơn giản và lâu đời nhất.
  • Chạy trong một luồng.
  • Dừng tất cả các luồng khác trong thời gian dọn dẹp (Stop-the-World).
  • Phù hợp cho ứng dụng nhỏ, không đa luồng mạnh.
  • Bật bằng cờ: -XX:+UseSerialGC

Parallel GC

  • Parallel GC (còn gọi là “Throughput Collector”).
  • Sử dụng nhiều luồng để dọn rác.
  • Tối ưu thông lượng tối đa.
  • Vẫn dọn với các lần dừng Stop-the-World, nhưng nhanh hơn Serial.
  • Phù hợp cho ứng dụng máy chủ, nơi các pause ngắn không quá quan trọng.
  • Bật bằng cờ: -XX:+UseParallelGC

CMS (Concurrent Mark Sweep)

  • CMS — đã lỗi thời nhưng từng rất phổ biến, tập trung giảm thiểu thời gian dừng.
  • Hoạt động bán song song với ứng dụng, giảm thời gian dừng.
  • Khó cấu hình hơn, có chi phí phụ trội.
  • Từ Java 9 bị đánh dấu là deprecated.
  • Bật bằng cờ: -XX:+UseConcMarkSweepGC

G1 (Garbage First)

  • G1 GC — bộ gom rác hiện đại mặc định (từ Java 9).
  • Cân bằng giữa thời gian dừng nhỏ và hiệu năng.
  • Chia heap thành nhiều vùng nhỏ (mô hình theo vùng).
  • Có thể thu gom chọn lọc theo từng vùng, không đụng tới toàn bộ heap.
  • Cho phép đặt mục tiêu thời gian dừng tối đa, ví dụ -XX:MaxGCPauseMillis=200.
  • Cờ bật: -XX:+UseG1GC (thường không cần, vì G1 là mặc định).

ZGC và Shenandoah

  • ZGCShenandoah — các bộ gom rác độ trễ thấp hiện đại.
  • Mục tiêu — pause tối thiểu (mili giây), ngay cả với heap rất lớn (tới terabyte).
  • Hoạt động gần như hoàn toàn song song với ứng dụng.
  • Yêu cầu Java 11+ (ZGC) hoặc Java 12+ (Shenandoah).
  • Phù hợp cho hệ thống nhạy cảm độ trễ (sàn giao dịch, fintech, phân tích thời gian thực).
  • Cờ bật: -XX:+UseZGC hoặc -XX:+UseShenandoahGC

3. Nguyên lý hoạt động của các GC hiện đại

Thế hệ trẻ và thế hệ già (Young/Old Generation)

JVM chia heap thành hai phần lớn:

Thế hệ trẻ (Young Generation): nơi chứa tất cả đối tượng mới. Việc dọn dẹp diễn ra thường xuyên và nhanh ( Minor GC ).

Thế hệ già (Old Generation, Tenured): nơi các đối tượng “sống sót” qua nhiều lần dọn ở thế hệ trẻ được chuyển sang. Ở đây dọn dẹp diễn ra ít thường xuyên hơn nhưng lâu hơn ( Major/Full GC ).

Vì sao? Phần lớn đối tượng trong Java sống rất ngắn (ví dụ, chuỗi tạm, collection trong phương thức). Do đó có thể dọn thế hệ trẻ nhanh và thường xuyên, không đụng tới thế hệ già.

Minor GC

  • Chỉ làm sạch thế hệ trẻ.
  • Nhanh, với pause ngắn.
  • Không ảnh hưởng tới các đối tượng ở thế hệ già.

Major (Full) GC

  • Làm sạch toàn bộ heap (cả thế hệ trẻ và già).
  • Có thể mất nhiều thời gian (giây hoặc hơn trên heap lớn).
  • Thường đi kèm với pause dài của ứng dụng.

GC xác định đối tượng nào cần xóa như thế nào?

GC tìm các đối tượng “sống” bắt đầu từ các tham chiếu gốc (root set): biến cục bộ trong stack của các luồng, trường tĩnh, tham số phương thức, v.v. Mọi thứ có thể “chạm tới” được coi là sống. Phần còn lại — rác.

4. So sánh các GC hiện đại: G1, ZGC, Shenandoah

Hãy xem chúng khác nhau thế nào. Bảng minh họa:

GC Mục tiêu chính Mô hình bộ nhớ Thời gian dừng tối thiểu Khả năng mở rộng Hỗ trợ Khi sử dụng
G1 Cân bằng giữa dừng/tốc độ Vùng ~10–200 ms Tới hàng trăm GB Java 9+ (mặc định) Đa số ứng dụng máy chủ
ZGC Tối thiểu hóa dừng Vùng, “coloring” <10 ms Tới terabyte Java 11+ Thời gian thực, nhạy cảm độ trễ
Shenandoah Tối thiểu hóa dừng Vùng, “coloring” <10 ms Tới terabyte Java 12+ (Red Hat) Thời gian thực, nhạy cảm độ trễ

G1 GC: Garbage First

  • Chia heap thành nhiều vùng (thường 1–32 MB mỗi vùng).
  • Trong lúc dọn, chọn các vùng có nhiều rác nhất (“garbage first”).
  • Có thể thu gom chỉ một phần heap, thay vì toàn bộ.
  • Cho phép đặt mục tiêu pause: -XX:MaxGCPauseMillis=200.
  • Phù hợp để cân bằng giữa tốc độ và pause; dùng mặc định từ Java 9.

Ví dụ bật (nếu bị tắt):

java -XX:+UseG1GC -jar myapp.jar

ZGC: Z Garbage Collector

  • Thử nghiệm ở Java 11, ổn định từ Java 15.
  • Hầu như không dừng ứng dụng: pause thường <10 ms, ngay cả với 1–2 TB heap.
  • Dùng “coloring” và con trỏ đặc biệt.
  • Yêu cầu JVM 64‑bit; không chạy trên hệ 32‑bit.
  • Hỗ trợ trên Linux, macOS, Windows.

Ví dụ bật:

java -XX:+UseZGC -jar myapp.jar

Shenandoah

  • Do Red Hat phát triển; mục tiêu tương tự ZGC.
  • Pause tối thiểu, làm việc song song tích cực với ứng dụng.
  • Hỗ trợ Linux và Windows; có trong một số bản OpenJDK.
  • Dùng kỹ thuật tương tự nhưng thuật toán nội bộ khác.

Ví dụ bật:

java -XX:+UseShenandoahGC -jar myapp.jar

So sánh trực quan

graph TD
    A[Thế hệ trẻ] -->|Minor GC| B[Thế hệ già]
    B -->|Major GC| C[GC Pause]
    D[G1: vùng] --> E[Vùng được chọn lọc]
    F[ZGC/Shenandoah: vùng] --> G[Dọn dẹp song song]

5. Thực hành: cách biết và thay đổi GC

Làm sao biết đang dùng GC nào?

  1. Nhật ký JVM: Chạy ứng dụng với tham số -Xlog:gc* (Java 9+) hoặc -verbose:gc (trước Java 8). Trong log sẽ thấy GC nào đang dùng và tần suất/độ dài các pause.
  2. jcmd: Thực thi:
    jcmd <pid> VM.flags
    
    trong đó <pid> — mã tiến trình Java (PID).
  3. jvisualvm: Trong mục “Monitoring” có thể xem loại GC.

Cách thay đổi GC cho ứng dụng của bạn?

Thêm cờ phù hợp khi chạy chương trình Java:

G1 GC (mặc định, có thể chỉ rõ):

java -XX:+UseG1GC -jar myapp.jar

ZGC:

java -XX:+UseZGC -jar myapp.jar

Shenandoah:

java -XX:+UseShenandoahGC -jar myapp.jar

Cách đặt kích thước heap và pause?

  • Kích thước heap tối đa: -Xmx2G
  • Kích thước heap tối thiểu: -Xms512M
  • Với G1: pause mục tiêu — -XX:MaxGCPauseMillis=200

Ví dụ lệnh chạy đầy đủ:

java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar

6. Lựa chọn GC cho các loại tác vụ

Khi nào chọn G1

  • Trong đa số ứng dụng server và desktop — lựa chọn mặc định rất tốt.
  • Hoạt động tốt với heap từ hàng trăm MB tới hàng trăm GB.
  • Cân bằng giữa tốc độ và pause.

Khi nào chọn ZGC hoặc Shenandoah

  • Nếu ứng dụng nhạy cảm với độ trễ (latency‑critical: sàn giao dịch, game online, phân tích thời gian thực).
  • Nếu heap rất lớn (hàng trăm GB trở lên).
  • Nếu chỉ chấp nhận các pause tối thiểu (mili giây).
  • Cần Java 11+ (ZGC) hoặc Java 12+ (Shenandoah).

Khi Parallel GC là đủ

  • Cho ứng dụng nhỏ, cần thông lượng tối đa và pause không quá quan trọng.
  • Cho xử lý batch, nơi có thể “chịu được” dừng khi Full GC.

7. Ví dụ: so sánh hành vi GC trên một ứng dụng đơn giản

Một ứng dụng nhỏ tạo rất nhiều đối tượng tạm thời (mô phỏng xử lý đơn hàng):

public class GCSimulator {
    public static void main(String[] args) {
        while (true) {
            // Tạo 100 000 đối tượng mỗi vòng lặp
            for (int i = 0; i < 100_000; i++) {
                String s = new String("Order-" + i);
            }
            // Nghỉ một chút
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
    }
}

Chạy nó với các GC khác nhau và xem log:

java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator

Bạn sẽ thấy gì?
G1 sẽ có các pause thường xuyên nhưng ngắn. ZGC/Shenandoah — pause còn ngắn hơn, nhưng có thể diễn ra thường xuyên hơn. Parallel GC — pause dài hơn nhưng ít hơn.

8. Lỗi phổ biến và lưu ý khi làm việc với GC

Lỗi số 1: Kỳ vọng GC giải quyết mọi vấn đề bộ nhớ. GC không phải là đũa thần. Nếu bạn giữ tham chiếu tới các đối tượng không cần thiết, không GC nào giúp được — sẽ rò rỉ bộ nhớ.

Lỗi số 2: Gọi cưỡng bức System.gc(). JVM biết rõ khi nào nên dọn rác. Gọi cưỡng bức GC có thể gây pause dài và giảm hiệu năng.

Lỗi số 3: Bỏ qua log GC. Nếu không theo dõi log GC, bạn có thể không nhận ra ứng dụng của mình thường xuyên “đơ” vì Full GC.

Lỗi số 4: Dùng GC đã lỗi thời. Ví dụ, CMS không còn được phát triển. Tốt hơn nên chuyển sang G1 hoặc các bộ gom rác low‑latency hiện đại.

Lỗi số 5: Chọn GC không phù hợp với bài toán. Nếu ứng dụng của bạn nhạy cảm độ trễ mà dùng Parallel GC — hãy chờ các pause dài. Nếu là xử lý batch mà bật ZGC — bạn sẽ chịu chi phí phụ trội không cần thiết.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION