← Về danh sách
Điện toán & Nhúng
#가비지컬렉션#메모리관리#세대별GC#STW#ZGC
Cập nhật lần cuối · 2026-10-12

Thu gom rác (Garbage Collection)

1. Tổng quan

A. Định nghĩa

Thu gom rác (GC) là kỹ thuật quản lý bộ nhớ tự động trong đó runtime tự động nhận diện và thu hồi các đối tượng trên heap mà chương trình không còn có thể truy cập tới (unreachable), để lập trình viên không phải giải phóng bộ nhớ một cách tường minh.

Lý do căn bản khiến GC ra đời nằm ở sự nguy hiểm của việc quản lý bộ nhớ thủ công. Trong các môi trường như C/C++ nơi lập trình viên chịu trách nhiệm cấp phát và giải phóng qua malloc/free, new/delete, nếu quên giải phóng thì rò rỉ bộ nhớ (memory leak) tích tụ khiến máy chủ chạy dài ngày chết dần, còn nếu dùng lại vùng đã giải phóng thì con trỏ treo (dangling pointer) và giải phóng hai lần (double free) gây ra hành vi không xác định và lỗ hổng bảo mật (Use-After-Free). Những lỗi như vậy khó tái hiện và chi phí gỡ lỗi khổng lồ. GC được tạo ra để chuyển "trách nhiệm theo dõi vòng đời (lifetime) của đối tượng" từ con người sang runtime, qua đó nâng cao năng suất và tính ổn định cùng lúc.

B. Bối cảnh ra đời và sự cần thiết

Lịch sử GC bắt đầu năm 1959 khi John McCarthy lần đầu đưa nó vào khi thiết kế LISP để xử lý tham chiếu vòng (cyclic reference) trong cấu trúc danh sách. Từ đó phần lớn các ngôn ngữ chủ lưu hiện đại—Smalltalk, Java, C#, Go, JavaScript, Python—đều tích hợp sẵn GC, và ngày nay đại đa số ứng dụng máy chủ, di động và web chạy trên nền GC. Phía sau là sự bùng nổ về quy mô và tuổi thọ của ứng dụng. Trong các hệ thống lớn nơi hàng trăm thư viện và hàng triệu đối tượng đan xen, việc con người theo dõi quyền sở hữu (ownership) của mọi đối tượng là gần như bất khả thi trên thực tế, và trên máy chủ chạy không gián đoạn nhiều tháng thì ngay cả một rò rỉ nhỏ cũng chí mạng.

Tuy nhiên GC không "miễn phí". Cái giá của tự động hóa là công việc thu hồi chen ngang tại thời điểm chạy, lúc đó xuất hiện khoảng STW (Stop-The-World) làm dừng các luồng ứng dụng, ảnh hưởng tới độ trễ (latency) và thông lượng (throughput). Vì vậy lịch sử phát triển của công nghệ GC hiện đại chính là sự đáp lời không ngừng cho câu hỏi "làm sao thu nhỏ và che giấu thời gian dừng mà vẫn giữ tính đúng đắn của thu hồi". Trong các hệ thống giao dịch tài chính hay dịch vụ tương tác coi trọng tính đáp ứng, ngay cả hàng chục mili-giây dừng cũng có thể vi phạm SLA, nên lựa chọn và tinh chỉnh GC trở thành quyết định thiết kế quan trọng dưới góc nhìn kỹ sư chuyên nghiệp.

Hơn nữa GC không chỉ dừng ở "chức năng dọn trống bộ nhớ" mà là một hệ thống con gắn khít với bộ cấp phát (allocator), runtime và thiết kế ngôn ngữ. Dùng chiến lược cấp phát nào (bump pointer hay danh sách rảnh phân tách), có cho phép di chuyển đối tượng không, có nhúng siêu dữ liệu vào con trỏ không—tất cả đều thay đổi các thuật toán GC khả thi. Do đó hiểu đúng GC là trang bị khung tư duy về toàn bộ việc quản lý bộ nhớ, bao trùm cấp phát bộ nhớ, bố trí đối tượng và hàng rào (barrier) runtime.

C. Đặc trưng của thu gom rác

GC có vài đặc trưng cốt yếu. Thứ nhất, dựa trên khả năng truy cập (reachability). "Rác" được định nghĩa không phải là "đối tượng sẽ không dùng trong tương lai" mà là "đối tượng không thể truy cập từ các gốc theo đường tham chiếu"; GC không dự đoán tương lai và xác định an toàn đối tượng cần thu hồi chỉ từ đồ thị tham chiếu hiện tại. Thứ hai, tính bảo thủ xấp xỉ: đối tượng truy cập được thì tuyệt đối không thu hồi (an toàn), nhưng đối tượng không truy cập được thì không phải lúc nào cũng thu hồi ngay (cho phép trì hoãn). Thứ ba, tính can thiệp runtime: khi áp lực cấp phát vượt ngưỡng, GC can thiệp bất đồng bộ ngay giữa luồng ứng dụng, khiến hồ sơ hiệu năng bị gián đoạn. Thứ tư, tận dụng thế hệ và tính cục bộ, tích cực khai thác giả thuyết thực nghiệm rằng đa số đối tượng chết nhanh (giả thuyết thế hệ yếu).

Những đặc trưng này nghĩa là GC không phải "phép màu giúp khỏi bận tâm bộ nhớ một khi đã có", mà là một hệ thống con quản lý tài nguyên có mô hình chi phí và hành vi rõ ràng. Điều quan trọng dưới góc nhìn kỹ sư chuyên nghiệp không phải là GC có tồn tại hay không, mà là năng lực hiểu và kiểm soát một cách định lượng việc nó can thiệp khi nào, bao nhiêu, theo cách nào để tác động tới độ trễ và thông lượng của ứng dụng. Dưới đây ta lần lượt xem xét từ nguyên lý vận hành của GC, các bộ thu gom siêu thấp độ trễ hiện đại, cho tới góc nhìn vận hành và tinh chỉnh chúng trong thực tiễn.

2. Nguyên lý cơ bản và cấu trúc tổng thể của thu gom rác

Điểm xuất phát của GC là tập gốc (root set). Gốc là "các tham chiếu mà chương trình cầm trực tiếp trong tay", như biến cục bộ trên ngăn xếp luồng, thanh ghi CPU, biến toàn cục/tĩnh. GC bắt đầu từ các gốc này, đi theo tham chiếu để đánh dấu mọi đối tượng truy cập được, và coi phần còn lại chưa đánh dấu là rác. Sơ đồ cấu trúc dưới đây cho thấy toàn cảnh, nhìn heap như một đồ thị tham chiếu.

flowchart LR
    subgraph Roots["Tập gốc(Root Set)"]
        S["Biến cục bộ ngăn xếp"]
        G["Biến toàn cục/tĩnh"]
        R["Thanh ghi"]
    end
    S --> A["Đối tượng A(truy cập được)"]
    G --> B["Đối tượng B(truy cập được)"]
    A --> C["Đối tượng C(truy cập được)"]
    B --> C
    D["Đối tượng D(rác)"] --> E["Đối tượng E(rác)"]
    E -.->|"tham chiếu vòng"| D
    style D fill:#f8d7da
    style E fill:#f8d7da

Việc xác định chính xác tập gốc khó hơn vẻ ngoài. Trình biên dịch phải cung cấp bản đồ ngăn xếp (stack map) cho biết tại mỗi điểm thực thi, khe ngăn xếp và thanh ghi nào thực sự giữ tham chiếu đối tượng, thì GC mới thực hiện được thu gom "chính xác (precise)". Trong môi trường thiếu thông tin đó (ví dụ quét ngăn xếp gốc một cách bảo thủ), người ta dùng GC bảo thủ (conservative GC), vốn có thể thiếu chính xác do nhầm giá trị số nguyên trông giống con trỏ thành tham chiếu và giữ sống đối tượng đã chết. Các runtime được quản lý như Java, C# chọn GC chính xác, còn một số GC cho C/C++ (Boehm GC) chọn phương thức bảo thủ.

Trong hình trên, D và E tham chiếu lẫn nhau nhưng không thể truy cập từ các gốc nên cả hai đều là rác. Ví dụ này cho thấy vì sao tham chiếu vòng (cyclic reference) lại quan trọng. Phương thức chỉ đếm "số tham chiếu trỏ tới tôi" (đếm tham chiếu) không bao giờ đưa số đếm của D và E về 0 vì chúng trỏ lẫn nhau, nên không bao giờ được thu hồi; còn phương thức truy vết dựa trên khả năng truy cập chỉ xét sự liên kết từ các gốc, nên lọc ra chính xác rác vòng này.

Thuật toán GC chia rộng thành phương thức đếm tham chiếu và phương thức truy vết (tracing). Hai cách tiếp cận khác nhau về triết lý ở chỗ "xác định rác khi nào, như thế nào".

  • Đếm tham chiếu: Xác định ngay khoảnh khắc một tham chiếu bị bỏ. Độ trễ phẳng nhưng không bắt được tham chiếu vòng và có chi phí cập nhật thường trực.
  • Truy vết: Xác định bằng cách quét định kỳ đồ thị từ các gốc. Thu hồi chính xác vòng nhưng dừng và tải trọng dồn vào lúc thu gom.

Runtime thực tế thường pha trộn cả hai. Chẳng hạn CPython mặc định là đếm tham chiếu nhưng cũng chạy kèm một bộ thu gom truy vết phụ cho rác vòng. Sơ đồ quy trình dưới đây cho thấy từng bước hoạt động của Mark-Sweep, cách truy vết tiêu biểu nhất.

flowchart TD
    Start["Cấp phát thất bại/đạt ngưỡng"] --> STW["STW: dừng luồng ứng dụng"]
    STW --> Mark["Giai đoạn Mark: duyệt đồ thị từ gốc, đánh dấu đối tượng sống"]
    Mark --> Sweep["Giai đoạn Sweep: thu hồi đối tượng chưa đánh dấu về danh sách rảnh"]
    Sweep --> Compact{"Phân mảnh nghiêm trọng?"}
    Compact -->|"Có"| Comp["Compact: nén đối tượng sống về một phía"]
    Compact -->|"Không"| Resume["Tiếp tục ứng dụng"]
    Comp --> Resume

Ở giai đoạn Mark (đánh dấu), đồ thị được duyệt theo chiều sâu/rộng từ các gốc, bật bit đánh dấu cho mỗi đối tượng được chạm tới. Chi phí duyệt này tỷ lệ với số đối tượng sống và mật độ tham chiếu, nên heap càng nhiều đối tượng sống thì đánh dấu càng tốn kém. Ở giai đoạn Sweep (dọn), toàn bộ heap được quét, các đối tượng chưa đánh dấu được trả về bộ nhớ rảnh, và bit đánh dấu được đặt lại cho lần thu gom sau. Chi phí Sweep tỷ lệ với tổng kích thước heap. Phương thức này có ưu điểm thu hồi chính xác tham chiếu vòng, nhưng sau thu hồi thì phân mảnh ngoài (fragmentation)—không gian rảnh vỡ vụn rải rác— tích tụ. Do đó Mark-Sweep-Compact dồn các đối tượng sống về một phía bộ nhớ sau khi Sweep để bảo đảm không gian rảnh liền mạch. Nén loại bỏ phân mảnh và cho phép cấp phát bump pointer, hoàn tất việc cấp phát kế tiếp chỉ bằng tăng con trỏ, nhưng kéo theo chi phí cập nhật tham chiếu do di chuyển đối tượng.

Điều cần nhớ ở đây là sự thật rằng độ dài dừng của GC không nhất thiết tỷ lệ với kích thước heap. Ở các bộ thu gom thời kỳ đầu, heap càng lớn thì thời gian STW toàn phần càng dài, nên với heap hàng trăm GB một lần Full GC có thể lên tới vài giây, khiến dịch vụ thực tế đứng im. Các bộ thu gom đồng thời và dựa trên vùng hiện đại lấy việc cắt đứt quan hệ tỷ lệ này làm mục tiêu cốt lõi, và đây chính là động cơ căn bản cho sự tiến hóa thuật toán được xem xét ở các phần sau.

3. Thuật toán cốt lõi — đếm tham chiếu, sao chép, thu gom theo thế hệ

A. Đếm tham chiếu (Reference Counting)

Đếm tham chiếu giữ trong phần đầu (header) của mỗi đối tượng "số tham chiếu trỏ tới tôi", tăng 1 khi sinh tham chiếu, giảm 1 khi bỏ, và thu hồi ngay khoảnh khắc số đếm về 0. Nó khác triết lý căn bản so với truy vết ở chỗ không có luồng thu gom riêng quét toàn bộ heap; thay vào đó logic thu hồi được phân tán vào phép gán tham chiếu, một thao tác hằng ngày. CPython và Swift (ARC, Automatic Reference Counting) là tiêu biểu.

Ưu điểm lớn nhất là tính tức thời và khả năng dự đoán. Vì thu hồi xảy ra ngay tại điểm rác phát sinh nên bộ nhớ được trả về nhanh chóng, và về nguyên lý không cần một khoảng dừng STW toàn phần nên phân bố độ trễ phẳng. Chi phí thu hồi cũng được rải nhỏ khắp chương trình, làm dịu "vách hiệu năng" đặc trưng của truy vết nơi bộ thu gom làm việc dồn dập vào một thời điểm. Vì vậy ARC được ưa dùng trên di động (iOS/Swift), nơi tính đáp ứng quan trọng.

Tuy nhiên đếm tham chiếu có những điểm yếu cấu trúc rõ ràng. Thứ nhất, như đã thấy ở trên, không tự thu hồi được tham chiếu vòng. Hai đối tượng trỏ lẫn nhau không bao giờ tụt dưới số đếm 1 và tồn tại mãi, nên CPython bố trí một bộ thu gom vòng (cyclic collector) chạy định kỳ, còn Swift bố trí tham chiếu weak/unowned do lập trình viên chỉ định làm biện pháp bổ trợ. Thứ hai, mỗi phép gán tham chiếu đều phải cập nhật số đếm nên có chi phí thường trực phủ lên, và trong đa luồng phải xử lý cập nhật này một cách nguyên tử (atomic) khiến chi phí tranh chấp dòng cache tăng. Thực tế, một trong những lý do CPython duy trì GIL (Global Interpreter Lock) lâu dài chính là vấn đề an toàn luồng này của đếm tham chiếu, và cũng là lý do thí nghiệm free-threaded CPython gần đây khó khăn. Thứ ba, khi tham chiếu cuối cùng tới một đồ thị đối tượng lớn bị bỏ, một chuỗi hủy nhiều đối tượng có thể xảy ra cùng lúc, tạo gai độ trễ tức thời.

B. Thu gom sao chép (Copying / Scavenging)

Thu gom sao chép chia heap thành hai nửa không gian (semi-space) From và To, và khi thu gom chỉ sao chép và nén các đối tượng sống của From sang To rồi dọn trống toàn bộ From một lần. Trong quá trình sao chép, nó để lại con trỏ chuyển tiếp (forwarding pointer) tại vị trí gốc của mỗi đối tượng sống trỏ tới vị trí mới, để nhiều tham chiếu trỏ cùng một đối tượng đều được cập nhật nhất quán sang địa chỉ mới.

Điểm mạnh quyết định của phương thức này là chi phí chỉ tỷ lệ với số đối tượng sống. Đối tượng chết hoàn toàn không bị chạm tới và không gian From bị vứt trọn, nên rác dù nhiều bao nhiêu thì thời gian thu gom cũng không tăng. Tức nó có đặc tính "càng nhiều rác càng nhanh" và cực kỳ có lợi ở vùng tỷ lệ sống sót thấp. Nén cũng hoàn tất đồng thời với sao chép nên phân mảnh ngoài bị loại trừ từ gốc, và vì không gian rảnh luôn liền mạch nên khả thi phương thức bump pointer hoàn tất cấp phát chỉ bằng tăng con trỏ. Một hệ quả phụ là các đối tượng tụ lại theo thứ tự cấp phát, cải thiện tính cục bộ tham chiếu (locality).

Mặt khác, sự lãng phí không gian chỉ dùng được một nửa bộ nhớ khả dụng là điểm yếu cốt yếu, vì luôn phải giữ trống một nửa không gian. Vì lý do này, hiếm khi áp dụng thu gom sao chép cho toàn bộ heap; thường chỉ áp dụng cục bộ cho riêng "vùng đối tượng trẻ", nơi tỷ lệ sống sót thấp khiến lợi ích của nó lớn. Cấu trúc thực tế trong đó thế hệ Young của JVM được chia thành Eden và hai không gian Survivor, sao chép những đối tượng sống sót của Eden cộng một Survivor sang Survivor kia, chính là một biến thể của thu gom sao chép này—một thiết kế làm dịu lãng phí nửa không gian bằng điều chỉnh tỷ lệ Survivor.

C. Thu gom theo thế hệ (Generational GC)

Thu gom theo thế hệ dựa trên giả thuyết thế hệ yếu (weak generational hypothesis) rằng "đa số đối tượng chết ngay sau khi tạo, còn đối tượng sống sót lâu thì sẽ còn sống lâu nữa". Heap được chia thành thế hệ Young (Eden+Survivor) và Old (Tenured), và mọi cấp phát xảy ra ở Eden. Bảng dưới đây tóm tắt phần cốt lõi của chiến lược theo thế hệ.

Phân loại Thế hệ Young(Minor GC) Thế hệ Old(Major/Full GC)
Đối tượng mục tiêu Đối tượng đoản mệnh vừa mới tạo Đối tượng trường thọ được thăng cấp sau nhiều lần sống sót
Tần suất thu gom Rất thường xuyên Hiếm
Thuật toán chính Thu gom sao chép Mark-sweep-compact
Chi phí thu gom Ngắn (ít đối tượng sống) Dài (vùng lớn, có thể STW)
Hiệu quả Thu hồi khối lượng lớn với dừng ngắn Dọn rò rỉ và phân mảnh của đối tượng trường thọ

Thế hệ Young có tỷ lệ sống sót thấp nên được quét nhanh bằng thu gom sao chép, và đối tượng sống sót quá một số lần nhất định thì được thăng cấp (promotion) lên Old. Thách thức cốt lõi là tham chiếu liên thế hệ. Nếu đối tượng Old trỏ tới đối tượng Young, thì khi chỉ thu gom Young phải coi cả đối tượng Old đó như một gốc. Vì vậy, hàng rào ghi (write barrier) ghi lại tham chiếu liên thế hệ vào Tập ghi nhớ (Remembered Set) hoặc bảng thẻ (card table), để tại lúc Minor GC đi theo tham chiếu không sót mà không cần quét toàn bộ Old. Nhờ cấu trúc này, GC theo thế hệ thu hồi phần lớn rác với dừng ngắn mà không cần lục tung toàn bộ heap.

Làm ví dụ thực tế, máy chủ xử lý yêu cầu web tạo rồi vứt vô số đối tượng DTO và chuỗi tạm thời cho mỗi yêu cầu, hầu hết là các đối tượng đoản mệnh điển hình chết ngay sau phản hồi. Trong tải trọng như vậy, GC theo thế hệ dọn trống Eden bằng một Minor GC trong vài mili-giây, và theo đo đạc, thường thì hơn 90% tổng số đối tượng được cấp phát được thu hồi ngay ở Minor GC đầu tiên. Ngược lại, các đối tượng sống lâu như phiên (session) và bộ nhớ đệm (cache) tụ về Old, nên một Major GC hiếm khi xảy ra sẽ dọn chúng cùng lúc. Việc tách đối tượng theo vòng đời như vậy để kết hợp "thường xuyên và rẻ" với "thi thoảng và đắt" chính là triết lý thiết kế của GC theo thế hệ.

D. Đánh dấu ba màu và tính nhất quán của đánh dấu đồng thời

Để hiểu đánh dấu đồng thời (concurrent), trừu tượng đánh dấu ba màu (tri-color marking) là thiết yếu. Mọi đối tượng được phân loại thành trắng (chưa phát hiện, ứng viên thu hồi), xám (đã phát hiện nhưng chưa duyệt tham chiếu con), đen (đã duyệt hết kể cả con), và cho đến khi màu xám cạn kiệt thì tiến trình đẩy lên bằng cách "tô xám thành đen và chuyển con của nó từ trắng sang xám". Khi đánh dấu kết thúc, các đối tượng còn lại màu trắng là rác.

Vấn đề phát sinh khi đánh dấu và ứng dụng chạy đồng thời. Nếu một đối tượng đã tô đen bị đổi để tham chiếu một đối tượng trắng mới, và đường màu xám từng trỏ tới cái trắng đó biến mất, thì xảy ra lỗi chí mạng khi một đối tượng sống bị nhầm là rác và bị thu hồi. Để ngăn điều này, GC chặn các thay đổi tham chiếu bằng hàng rào ghi (write barrier) để giữ bất biến. Các chiến lược tiêu biểu là cập nhật tăng dần (incremental update), tô xám tham chiếu mới đang được đổi, và SATB (Snapshot-At-The-Beginning), bảo tồn tham chiếu cũ đang bị bỏ; G1 chọn SATB. Nhờ cơ chế này, các bộ thu gom đồng thời như ZGC và Shenandoah có thể hoàn tất đánh dấu an toàn mà gần như không dừng ứng dụng.

4. GC hiện đại giảm thời gian dừng — đồng thời, song song, dựa trên vùng

Nếu GC thời kỳ đầu là STW toàn phần, thì GC hiện đại hoặc thu gom đồng thời (concurrent) với ứng dụng hoặc chẻ nhỏ công việc để phân tán các lần dừng. So sánh các bộ thu gom chính xoay quanh hệ sinh thái Java như sau.

Bộ thu gom Chiến lược Dừng mục tiêu Đặc điểm
Serial GC STW đơn luồng Tương đối dài Heap nhỏ, môi trường CPU đơn
Parallel GC STW đa luồng Dài (ưu tiên thông lượng) Tối ưu batch, thông lượng
CMS (deprecated) Mark-sweep đồng thời Trung bình Vấn đề phân mảnh, concurrent mode failure
G1 GC Dựa trên vùng (Region) Dự báo (hàng trăm ms → hàng chục ms) Chia heap thành region, ưu tiên region nhiều rác
ZGC / Shenandoah Nén đồng thời Hướng tới dưới 1ms Con trỏ load/colored, siêu thấp độ trễ trên heap lớn

G1 (Garbage First) chia nhỏ heap thành các vùng (region) cùng kích thước, theo dõi tỷ lệ rác của mỗi vùng, và thu gom từ các vùng có hiệu suất thu hồi cao trước (garbage-first), qua đó cố gắng giữ một cách dự báo thời gian dừng mục tiêu mà người dùng chỉ định. ZGC và Shenandoah tiến thêm một bước, thực hiện cả việc nén di chuyển đối tượng một cách đồng thời với ứng dụng, ràng các lần dừng về mức khoảng 1ms bất kể kích thước heap. Các công nghệ cốt lõi làm được điều này là hàng rào load (load barrier) và con trỏ màu (colored pointer) nhúng bit trạng thái vào con trỏ, ánh xạ lại (remapping) trong suốt sang địa chỉ mới nhất khi ứng dụng đọc một đối tượng đang bị di chuyển.

Người vận hành cần biết rằng các chế độ thất bại tồn tại ngay cả với GC đồng thời, dựa trên vùng. Vì ứng dụng vẫn tiếp tục cấp phát ngay cả khi thu gom đồng thời đang diễn ra, nên nếu tốc độ thu gom không bắt kịp tốc độ cấp phát, không gian Old đầy trước, sinh ra concurrent mode failure hoặc promotion failure (thất bại thăng cấp). Lúc đó runtime quay về Full GC STW toàn phần như biện pháp cuối cùng, và chính khoảnh khắc này được quan sát là lần dừng dài nhất trong một dịch vụ siêu thấp độ trễ. Do đó cốt lõi của việc tinh chỉnh GC đồng thời không phải là "đưa dừng về 0" mà là điều chỉnh khoảng dư của heap và ngưỡng bắt đầu thu gom để bộ thu gom không tụt lại sau áp lực cấp phát. Trong thực tiễn, người ta lấy tần suất của sự kiện thất bại này từ nhật ký GC làm chỉ số cốt lõi để điều chỉnh kích thước heap và tham số bộ thu gom.

Một khái niệm nhất thiết phải chỉ ra ở đây là STW và điểm an toàn (safepoint). Để GC thấy đồ thị tham chiếu một cách nhất quán, mọi luồng phải dừng tạm tại "một điểm nơi trạng thái heap nhất quán", và điểm này là điểm an toàn. Ngay cả GC đồng thời cũng đưa mọi luồng về điểm an toàn trong những khoảnh khắc ngắn như lúc bắt đầu và kết thúc đánh dấu, nên phát biểu chính xác không phải là "hoàn toàn không có STW" mà là "STW rất ngắn và không tỷ lệ với kích thước heap". Ngoài ra, rò rỉ logic phát sinh từ việc không bỏ tham chiếu tới các đối tượng trường thọ (ví dụ đối tượng vẫn đăng ký trong cache/listener nên GC không thể thu hồi) là vấn đề xảy ra ngay cả khi có GC, cho thấy rõ giới hạn rằng GC chỉ thu hồi cái "không truy cập được", chứ không phải cái "vô dụng".

5. Chuyên sâu — Xu hướng mới nhất và trường hợp áp dụng thực tiễn

Công nghệ GC đang tiến hóa nhanh theo các trục siêu thấp độ trễ, dung lượng lớn và khả năng quan sát. Ở phía Java, ZGC đã được mở rộng thành dạng theo thế hệ (Generational ZGC, chính thức ở JDK 21) để nắm bắt cả độ trễ thấp lẫn thông lượng, và Shenandoah cũng đã đưa vào chế độ theo thế hệ. Đây là xu hướng làm dịu quan niệm cũ rằng "GC đồng thời hy sinh thông lượng" bằng cách kết hợp với giả thuyết thế hệ yếu. Trong khi đó, Go dùng một GC mark-sweep đồng thời được thiết kế với độ trễ thấp làm ưu tiên hàng đầu, từ bỏ nén và thay vào đó kìm phân mảnh bằng bộ cấp phát phân lớp theo kích thước (size-classed allocator). Việc Go từ bỏ nén minh họa rõ rằng "mọi lựa chọn đều có cái giá". Không có nén thì dừng ngắn, nhưng phải chấp nhận phân mảnh và chi phí đường cấp phát, bù lại bằng thiết kế bộ cấp phát bộ nhớ. Với .NET, GC máy chủ, GC máy trạm và GC nền có thể chuyển đổi tùy tình huống, nên hành vi GC có thể đổi theo mục tiêu thông lượng/độ trễ ngay trong cùng một runtime. Gần đây đã xuất hiện bộ điều phối GC (pacer) điều chỉnh thời điểm thu gom dựa trên tỷ lệ cấp phát, và giới hạn bộ nhớ mềm (soft memory limit) phản ứng với áp lực bộ nhớ phần cứng thực tế, đáp ứng nhu cầu thực tiễn liên kết GC với giới hạn bộ nhớ của container đám mây.

Làm trường hợp áp dụng thực tiễn, có thể nêu container JVM trong môi trường Kubernetes. Trước đây JVM không nhận biết giới hạn bộ nhớ cgroup của container và định cỡ heap theo tổng bộ nhớ của host, gây OOM Kill; JVM mới nhất, với nhận biết container (UseContainerSupport), đọc giới hạn cgroup và đặt heap theo tỷ lệ (MaxRAMPercentage). Các dịch vụ coi trọng độ trễ đuôi (tail latency) như cổng API tương tác và máy chủ thanh toán có nhiều trường hợp được báo cáo về việc chuyển từ G1 sang ZGC để hạ dừng P99 từ hàng trăm ms xuống còn một chữ số ms. Ngược lại, khi tổng thông lượng là cốt lõi như batch ban đêm và pipeline dữ liệu, Parallel GC vẫn có lợi. Như vậy không có "GC đúng"; cốt lõi là lựa chọn khớp với đặc tính độ trễ, thông lượng và bộ nhớ của tải trọng, cùng tinh chỉnh dựa trên đo đạc.

Khả năng quan sát của việc vận hành GC cũng trở nên quan trọng. Thu thập nhật ký GC để giám sát tỷ lệ cấp phát, tần suất Minor/Major, tỷ lệ thăng cấp và phân bố dừng (P99/P99.9) và phát hiện sớm thất bại thăng cấp, concurrent mode failure và Full GC thường xuyên đã trở thành điều cơ bản của thực tiễn SRE. Chẳng hạn nếu đuôi độ trễ phản hồi của một dịch vụ bỗng dài ra, dù không có thay đổi mã ứng dụng, thì nguyên nhân thường là một đợt tăng vọt tỷ lệ cấp phát khiến Minor GC trở nên quá thường xuyên, hoặc một rò rỉ bộ nhớ làm đầy Old khiến Full GC lặp lại. Lúc đó, kết hợp nhật ký GC với phân tích heap dump (ví dụ truy vết các đối tượng giữ bộ nhớ lớn qua cây thống trị dominator tree) có thể xác định chính xác nguyên nhân, nên tính khả kiến của GC được dùng như công cụ chẩn đoán tuyến đầu khi ứng phó sự cố hiệu năng.

6. Cân nhắc và hàm ý

Dưới góc nhìn kỹ sư chuyên nghiệp, GC phải được coi là quyết định kiến trúc chi phối hiệu năng, chi phí và tính ổn định của hệ thống, vượt ra ngoài việc tự động hóa bộ nhớ đơn thuần.

  • Đánh đổi độ trễ với thông lượng: GC đồng thời, dựa trên vùng giảm các lần dừng nhưng tiêu tốn CPU/thông lượng qua hàng rào ghi/load và luồng bổ sung. Phải định nghĩa bản chất của SLA (thông lượng trung bình hay độ trễ đuôi) trước rồi mới chọn bộ thu gom, và lựa chọn luôn phải được kiểm chứng bằng thử tải và phân tích nhật ký GC.
  • Cân bằng kích thước heap và chi phí bộ nhớ: GC càng nhiều không gian rảnh càng hiệu quả vì tần suất giảm, nhưng bộ nhớ gắn trực tiếp với chi phí đám mây. Thay vì phình heap một cách mù quáng, thiết kế hạ tỷ lệ cấp phát (tái sử dụng đối tượng, pooling, streaming) là phương thuốc căn bản, và đặc biệt trong container phải nhất thiết liên kết giới hạn cgroup với GC.
  • Rò rỉ logic và trách nhiệm thiết kế: Ngay cả khi có GC, rò rỉ vẫn xảy ra ở nơi đối tượng bị ràng trong cache toàn cục, listener sự kiện hoặc ThreadLocal và không được thu hồi. Phải đưa quản lý vòng đời tường minh (tham chiếu yếu weak reference, giải phóng tường minh, chính sách hết hạn cache) vào thiết kế, và sự phụ thuộc vào GC không thể là tấm vé miễn trừ cho thiết kế bộ nhớ.
  • Liên kết với lựa chọn ngôn ngữ/runtime: Ở các lĩnh vực điều khiển và giao dịch tần suất cao nơi tính thời gian thực cực kỳ quan trọng, bản thân các lần dừng GC không được phép, nên người ta chọn mô hình không-GC dựa trên quyền sở hữu (ownership) của Rust, hoặc các kỹ thuật off-heap/tái sử dụng đối tượng. Ngược lại, với backend doanh nghiệp nơi năng suất và ổn định được ưu tiên, các ngôn ngữ có GC là hợp lý. GC phải được thiết kế cùng như một phần của chiến lược ngôn ngữ/runtime khớp với miền bài toán và các yêu cầu phi chức năng.
  • Tính hai mặt về góc nhìn bảo mật: GC loại trừ về mặt cấu trúc các lỗ hổng an toàn bộ nhớ như Use-After-Free và double free, giảm đáng kể bề mặt tấn công. Mặt khác, mẫu hình xuất hiện của các lần dừng GC có thể bị quan sát như một kênh bên (side channel), và thông tin nhạy cảm (như khóa mật mã) có thể lộ qua heap dump do còn lưu lại trên heap đến tận lúc thu hồi. Do đó dữ liệu nhạy cảm về bảo mật cần được bổ trợ bằng cách ghi đè tường minh bằng số 0 ngay sau khi dùng, hoặc quản lý off-heap.
  • Triển vọng: Sự phổ cập của GC đồng thời theo thế hệ, tăng cường nhận biết container/NUMA/heap dung lượng lớn, tự động tinh chỉnh tham số GC dựa trên học máy, và sự cùng tồn tại của mô hình quyền sở hữu (Rust) với GC tự động sẽ là các xu hướng cốt lõi trong thời gian tới. Kỹ sư chuyên nghiệp cần thiết lập trong tổ chức một triết lý vận hành nhìn GC không như "thứ tự nó chạy được" mà như đối tượng để quan sát, đo đạc và tinh chỉnh.

Tài liệu tham khảo


Tóm tắt một câu: Thu gom rác là kỹ thuật quản lý bộ nhớ tự động thu hồi các đối tượng rác dựa trên khả năng truy cập; nó đã tiến hóa từ các chiến lược đếm tham chiếu, truy vết, sao chép và theo thế hệ thành các bộ thu gom siêu thấp độ trễ đồng thời, dựa trên vùng như G1 và ZGC, và việc tinh chỉnh sự đánh đổi giữa độ trễ, thông lượng và chi phí bộ nhớ trên cơ sở đo đạc là cốt lõi dưới góc nhìn kỹ sư chuyên nghiệp.