09/05/2025 - 15:26 · 10 Min read

Bàn về Garbage Collection và Concurrent Mark-Sweep

Phân tích các nguyên lý nền tảng của Garbage Collection. Sau đó đi sâu mô tả chi tiết thuật toán Concurrent Mark-Sweep trong Go

Bàn về Garbage Collection và Concurrent Mark-Sweep
Việc quản lý tài nguyên bộ nhớ một cách thủ công, điển hình qua các lời gọi hàm như malloc và free trong C, hoặc thông qua các toán tử new và delete trong C++, từ lâu đã được nhận diện là một nguồn gốc tiềm tàng của nhiều bug nghiêm trọng trong quá trình phát triển phần mềm. Ví dụ như:
  • Memory leaks (rò rỉ bộ nhớ)

  • Dangling pointers (con trỏ lơ lửng)

  • Double frees (giải phóng kép)

Những khiếm khuyết như vậy làm suy giảm tính ổn định của ứng dụng mà còn có thể tạo ra các lỗ hổng bảo mật (mà tôi cũng kha khá lần tìm cách khai thác 🤪)

Để giải quyết những thách thức này, cơ chế Thu Hồi Bộ Nhớ Tự Động - Garbage Collection (GC), đã được phát triển. Mục tiêu cốt lõi của GC là tự động hóa quy trình xác định và thu hồi các vùng nhớ không còn được chương trình, hay còn gọi là mutator, tham chiếu đến. Mặc dù GC mang lại những lợi ích đáng kể về mặt năng suất và độ tin cậy, bản thân nó không phải là một giải pháp không có sự đánh đổi. Các thuật toán GC khác nhau sẽ có những đặc tính riêng biệt về hiệu năng, độ phức tạp và tác động lên hệ thống.

Bài viết này tập trung vào việc phân tích các nguyên lý nền tảng của Garbage Collection. Một phần quan trọng sẽ được dành để mô tả chi tiết thuật toán Concurrent Mark-Sweep, một kỹ thuật thu hồi bộ nhớ tiên tiến được ứng dụng rộng rãi trong nhiều hệ thống phần mềm hiện đại, bao gồm cả runtime của Go (my fav programming language 🥰)

1. Sự Cấp Thiết và Mục Đích Của Cơ Chế Thu Hồi Bộ Nhớ Tự Động

Sự phức tạp và nguy cơ tiềm ẩn lỗi trong việc quản lý bộ nhớ thủ công là động lực chính thúc đẩy sự ra đời và phát triển của các hệ thống GC. Memory leaks, nếu không được kiểm soát, sẽ dẫn đến việc tiêu thụ toàn bộ tài nguyên bộ nhớ khả dụng của hệ thống theo thời gian. Dangling pointers, khi được truy xuất, có thể dẫn đến hành vi không thể dự đoán, từ việc đọc dữ liệu không hợp lệ đến việc ghi đè lên các cấu trúc dữ liệu quan trọng khác. Double frees thường gây ra sự hỏng hóc nghiêm trọng trong cấu trúc dữ liệu nội bộ của trình quản lý bộ nhớ, có khả năng dẫn đến sụp đổ chương trình.

Garbage Collection được thiết kế để giảm thiểu hoặc loại bỏ các loại lỗi này bằng cách tự động hóa quy trình quản lý. Điều này mang lại nhiều lợi ích, bao gồm việc tăng năng suất của lập trình viên do họ không còn phải trực tiếp quản lý vòng đời của từng đối tượng bộ nhớ. Hơn nữa, GC góp phần làm giảm thiểu đáng kể một lớp lỗi phổ biến và thường rất khó để gỡ rối, từ đó tăng cường tính ổn định và an toàn tổng thể của phần mềm. Tuy nhiên, sự tự động hóa này đi kèm với một chi phí nhất định, đặc biệt là khả năng GC gây ra các khoảng dừng (pause) trong quá trình thực thi của ứng dụng để thực hiện công việc của mình.

2. Nguyên Tắc Vận Hành Của Garbage Collection

Về cơ bản, bất kỳ hệ thống GC nào cũng phải giải quyết hai nhiệm vụ chính:

  1. Identification of Liveness (xác định tính "sống" của đối tượng): Phân biệt giữa các objects vẫn còn có thể được truy cập bởi chương trình (live objects) và các objects không còn khả năng truy cập (garbage). Một object được coi là "live" nếu nó có thể được tiếp cận từ một tập hợp các "root" (ví dụ: biến toàn cục, các biến trên stack của các threads hoặc goroutines đang hoạt động, thanh ghi CPU).

  2. Memory Reclamation (thu hồi bộ nhớ): Sau khi đã xác định được các objects garbage, GC phải thu hồi không gian bộ nhớ mà chúng chiếm dụng. Vùng nhớ này sau đó sẽ được trả về cho hệ thống để có thể tái sử dụng cho các yêu cầu cấp phát bộ nhớ mới trong tương lai.

Việc triển khai một hệ thống GC hiệu quả và chính xác đòi hỏi phải đáp ứng nhiều tiêu chí quan trọng. Correctness là yêu cầu tối thượng: GC tuyệt đối không được phép thu hồi bất kỳ live object nào. Efficiency của GC được đánh giá qua nhiều khía cạnh, bao gồm CPU overhead, memory overhead, và tác động lên application throughput. Pause times là một yếu tố cực kỳ quan trọng, đặc biệt đối với các ứng dụng tương tác hoặc có yêu cầu low-latency; nhiều thuật toán GC yêu cầu dừng toàn bộ chương trình ứng dụng (Stop-The-World - STW) để đảm bảo tính nhất quán trong quá trình hoạt động. Fragmentation là hiện tượng heap bị chia cắt thành nhiều khối nhớ trống nhỏ, rời rạc sau nhiều chu kỳ cấp phát và thu hồi, làm giảm hiệu quả sử dụng bộ nhớ. Cuối cùng, Scalability đề cập đến khả năng của GC hoạt động hiệu quả với kích thước heap lớn và trên các hệ thống có nhiều CPU cores.

3. Các Chiến Lược Garbage Collection Cơ Bản

Trước khi đi sâu vào Concurrent Mark-Sweep, việc xem xét một số chiến lược GC nền tảng là cần thiết để hiểu rõ bối cảnh và sự phát triển của lĩnh vực này.

Một trong những phương pháp sớm nhất là Reference Counting. Trong cơ chế này, mỗi object duy trì một bộ đếm ghi lại số lượng tham chiếu đang trỏ đến nó. Khi một tham chiếu mới được tạo, bộ đếm tương ứng sẽ tăng lên; ngược lại, khi một tham chiếu bị hủy, bộ đếm sẽ giảm xuống. Nếu bộ đếm của một object đạt giá trị không, object đó được coi là garbage và có thể được thu hồi ngay lập tức. Ưu điểm chính của Reference Counting là việc thu hồi rác diễn ra một cách từ từ và phân tán, thường không gây ra các STW pause dài. Tuy nhiên, nó có một nhược điểm cố hữu là không thể tự xử lý các Circular References, nơi một nhóm các objects tham chiếu lẫn nhau tạo thành một chu trình kín mà không có tham chiếu nào từ bên ngoài trỏ vào. Ngoài ra, việc cập nhật bộ đếm cho mỗi thao tác gán con trỏ có thể gây ra overhead đáng kể, đặc biệt trong môi trường đa luồng nơi các thao tác này cần phải được thực hiện một cách atomic. Các ngôn ngữ như Python (trong CPython implementation), Swift (thông qua Automatic Reference Counting - ARC), và PHP (trong Zend Engine) sử dụng các biến thể của Reference Counting, thường được kết hợp với một cơ chế tracing GC định kỳ để giải quyết vấn đề tham chiếu vòng.
Writing a Mark-Sweep Garbage Collector – Dmitry Soshnikov

Nhóm thuật toán thứ hai, và là nhóm mà Concurrent Mark-Sweep thuộc về, là Tracing Garbage Collection. Nguyên tắc chung của các thuật toán này là bắt đầu từ tập hợp các "root" và "dò theo dấu vết" (trace) tất cả các con trỏ để tìm ra toàn bộ các objects có thể truy cập được. Những objects không thể truy cập được từ các root sẽ được xác định là garbage. Ưu điểm nổi bật của Tracing GC là khả năng xử lý các Circular References một cách tự nhiên. Các thuật toán tracing phổ biến bao gồm Mark-Sweep, Mark-Compact, và Copying GC.

4. Phân Tích Chi Tiết Thuật Toán Mark-Sweep Truyền Thống

Thuật toán Mark-Sweep là một trong những thuật toán Tracing GC đầu tiên và có cấu trúc tương đối đơn giản. Quá trình hoạt động của nó bao gồm hai giai đoạn chính.

Giai đoạn đầu tiên là Mark Phase. Trong giai đoạn này, GC bắt đầu từ các root objects và sử dụng một thuật toán duyệt đồ thị, thường là Depth-First Search (DFS) hoặc Breadth-First Search (BFS), để duyệt qua tất cả các objects có thể truy cập. Mỗi object được truy cập sẽ được "đánh dấu" (ví dụ, bằng cách thiết lập một bit cụ thể trong header của object) để chỉ ra rằng nó là một live object.

Giai đoạn thứ hai là Sweep Phase. Sau khi Mark Phase hoàn tất, GC sẽ quét toàn bộ heap. Bất kỳ object nào không được đánh dấu trong Mark Phase sẽ được coi là garbage. Bộ nhớ của các objects garbage này sau đó được thu hồi và thường được thêm vào một "free list" để có thể được tái sử dụng cho các yêu cầu cấp phát bộ nhớ trong tương lai.

Mặc dù Mark-Sweep truyền thống có ưu điểm là xử lý tốt Circular References và tương đối dễ triển khai, nó có một nhược điểm nghiêm trọng khi được thực hiện dưới dạng Stop-The-World. Toàn bộ chương trình ứng dụng phải dừng hoàn toàn trong cả Mark Phase và Sweep Phase. Với kích thước heap lớn, thời gian dừng này có thể kéo dài đáng kể, gây ảnh hưởng tiêu cực đến trải nghiệm người dùng và hiệu năng của hệ thống. Một vấn đề khác của Mark-Sweep là Fragmentation, do nó không di chuyển các objects trong quá trình thu hồi, dẫn đến việc heap có thể bị phân mảnh theo thời gian.

5. Thuật Toán Concurrent Mark-Sweep: Hướng Tới Giảm Thiểu Thời Gian Dừng

Ý tưởng cốt lõi đằng sau thuật toán Concurrent Mark-Sweep (CMS) là thực hiện phần lớn công việc của Mark Phase, và trong một số trường hợp cả Sweep Phase, một cách đồng thời (concurrently) với quá trình thực thi của chương trình ứng dụng (mutator). Mục tiêu chính là giảm thiểu tối đa thời gian mà ứng dụng phải dừng hoàn toàn (STW pause).

Để quản lý trạng thái của các objects trong quá trình marking đồng thời một cách an toàn và chính xác, CMS thường sử dụng một khái niệm trừu tượng được gọi là Tri-color Abstraction, do Dijkstra và các cộng sự đề xuất. Theo mô hình này, mỗi object trong heap được gán một trong ba màu:

  • White: Objects chưa được GC ghé thăm. Ban đầu, tất cả objects (trừ root) đều là White. Cuối Mark phase, các objects White còn lại là garbage.

  • Gray: Objects đã được GC ghé thăm (đã được đánh dấu là live) nhưng các con trỏ của nó trỏ đến các objects khác chưa được quét hết. Các objects Gray nằm trên "biên giới" của quá trình quét. GC lấy object từ tập Gray để xử lý.

  • Black: Objects đã được GC ghé thăm và tất cả các con trỏ của nó cũng đã được quét (tức là các objects mà nó trỏ tới đã được tô Gray hoặc Black). Object Black là an toàn, chắc chắn sống.

Để đảm bảo tính đúng đắn của quá trình marking đồng thời, tức là không thu hồi nhầm một live object, GC phải duy trì một quy tắc bất biến quan trọng, được gọi là Tri-color Invariant: không bao giờ được phép tồn tại một con trỏ từ một object Black trỏ trực tiếp đến một object White. Nếu quy tắc này bị vi phạm, object White đó có thể bị bỏ sót trong quá trình quét và bị thu hồi nhầm.

Để hiểu rõ tại sao quy tắc này lại quan trọng đến vậy, hãy xem xét điều gì sẽ xảy ra nếu nó bị vi phạm:

  1. Object Black: Một object được tô màu Black có nghĩa là GC đã hoàn tất việc quét chính nó và tất cả các đối tượng mà nó trực tiếp tham chiếu đến (tại thời điểm quét). GC sẽ không quay lại xem xét một object Black một lần nữa trong cùng một chu kỳ GC đó.

  2. Object White: Một object White là một ứng cử viên tiềm năng để bị thu hồi, vì GC chưa "nhìn thấy" nó thông qua một đường dẫn từ các root.

  3. Vi phạm Invariant: Giả sử, tại một thời điểm nào đó, mutator thực hiện một thao tác ghi, khiến một object A (đã là Black) tạo ra một tham chiếu mới đến một object B (vẫn còn White). Đồng thời, giả sử mutator cũng loại bỏ tất cả các đường dẫn tham chiếu khác (nếu có) từ các object Gray hoặc từ các root đến B.

Lúc này, chúng ta có tình huống A (Black) -> B (White).

  • Vì A đã là Black, GC sẽ không quét lại A để tìm ra tham chiếu mới này đến B.

  • Vì không còn đường dẫn nào khác từ các object Gray hoặc root đến B (theo giả định của chúng ta), B sẽ không bao giờ được tô màu Gray.

  • Do đó, khi giai đoạn marking kết thúc, B vẫn sẽ là White.

  • Trong giai đoạn Sweep, tất cả các object White sẽ bị coi là garbage và bị thu hồi.

    Tuy nhiên, B trong trường hợp này rõ ràng là một live object vì nó vẫn có thể được truy cập thông qua A. Việc thu hồi B sẽ dẫn đến một lỗi nghiêm trọng (dangling pointer khi A cố gắng truy cập B sau này).

Cơ chế then chốt để duy trì Tri-color Invariant trong khi mutator vẫn đang chạy và có thể thay đổi cấu trúc tham chiếu của heap là Write Barriers. Write Barrier là một đoạn mã nhỏ, thường được compiler tự động chèn vào trước mỗi thao tác ghi một con trỏ trong mã của ứng dụng. Mục đích chính của Write Barrier là "can thiệp" vào các thao tác ghi con trỏ của mutator để phát hiện và xử lý các tình huống có khả năng vi phạm Tri-color Invariant.

Khi một thao tác ghi như A.field = B xảy ra, và nếu thao tác này có nguy cơ tạo ra một liên kết "Black -> White", Write Barrier sẽ thực hiện một hành động để bảo vệ B khỏi việc bị thu hồi nhầm. Hành động này thường là một trong hai cách (tùy thuộc vào loại Write Barrier):

  • Tô màu Gray cho đối tượng White (B): Nếu A là Black và B là White, Write Barrier sẽ chủ động tô màu B thành Gray và đưa nó vào hàng đợi quét của GC. Bằng cách này, GC sẽ đảm bảo ghé thăm và xử lý B, sau đó tô B thành Black nếu B thực sự sống. Đây là cách tiếp cận của Incremental Update Write Barrier.

  • Tô màu Gray cho đối tượng bị ghi đè (nếu là White) hoặc đối tượng chứa (nếu là Black): Một số loại Write Barrier (như SATB) có thể có logic khác, ví dụ như nếu A là Black, nó có thể tô lại A thành Gray để GC phải quét lại nó, hoặc nếu giá trị của A.field trỏ đến một object White, nó sẽ tô Gray object cũ đó. Mục tiêu cuối cùng vẫn là đảm bảo không có object sống nào bị "mất dấu".

Bằng cách này, Tri-color Invariant được duy trì trong suốt quá trình concurrent marking. Nó là một điều kiện tiên quyết để đảm bảo rằng, vào cuối giai đoạn marking, tập hợp các object White thực sự chỉ chứa garbage, và không có một live object nào bị bỏ sót. Sự phức tạp của việc thiết kế và triển khai các Write Barrier hiệu quả (có overhead thấp nhưng vẫn đảm bảo tính đúng đắn) là một trong những thách thức lớn nhất trong việc xây dựng các hệ thống Concurrent GC.

6. Các Giai Đoạn Vận Hành Của Concurrent Mark-Sweep Trong Thực Tế (Tham Khảo Mô Hình Go GC)

Một chu kỳ hoạt động điển hình của Concurrent Mark-Sweep, ví dụ như trong runtime của Go, thường bao gồm các giai đoạn sau:

  1. Mark Setup hoặc Initial Mark: Một STW pause rất ngắn. Mục đích của giai đoạn này là dừng mutator tạm thời để kích hoạt write barrier và quét các root objects. Các objects được tìm thấy từ root sẽ được tô Gray và đưa vào hàng đợi xử lý.

  2. Concurrent Marking: Đây là giai đoạn chính và tốn nhiều thời gian nhất. Các GC worker (là các goroutines hoặc threads riêng biệt của GC) bắt đầu làm việc. Chúng liên tục lấy các objects Gray ra khỏi hàng đợi, quét các con trỏ của chúng, tô object đó thành Black, và tô các objects White mà nó trỏ tới thành Gray, sau đó đưa chúng vào hàng đợi. Mutator vẫn tiếp tục chạy đồng thời. Mỗi khi mutator thực hiện một thao tác ghi con trỏ, write barrier sẽ được kích hoạt để đảm bảo Tri-color Invariant được duy trì. Mặc dù giai đoạn này không làm dừng ứng dụng, các GC worker vẫn tiêu tốn tài nguyên CPU và có thể gây ra một sự suy giảm nhẹ về hiệu năng do cạnh tranh tài nguyên với mutator.

  3. Mark Termination, Final Mark, hoặc Remark: Một STW pause ngắn nữa. Mục đích của giai đoạn này là dừng mutator để xử lý nốt các objects còn lại trong hàng đợi Gray. Những objects này thường được thêm vào hàng đợi do các hoạt động của write barrier xảy ra gần cuối giai đoạn Concurrent Marking. Sau khi xử lý xong, write barrier sẽ được tắt. Giai đoạn này đảm bảo rằng tất cả các live objects đều đã được tô Black.

  4. Concurrent Sweeping: Sau khi Mark Termination, tất cả các objects còn lại trong heap mà vẫn còn màu White được xác định là garbage. Giai đoạn sweeping sẽ đi qua heap và thu hồi bộ nhớ của các White objects, thường là bằng cách thêm chúng vào một free list. Trong Go, giai đoạn sweeping cũng diễn ra đồng thời với mutator. Thậm chí, khi một goroutine của ứng dụng cần cấp phát bộ nhớ mới và không gian trống hiện tại không đủ, goroutine đó có thể tự mình thực hiện một phần công việc sweeping để giải phóng không gian.

7. Đánh Giá Ưu Nhược Điểm Của Thuật Toán Concurrent Mark-Sweep

Ưu điểm nổi bật nhất của Concurrent Mark-Sweep là khả năng giảm thiểu đáng kể thời gian STW pause. Thay vì dừng ứng dụng trong toàn bộ quá trình marking và sweeping, STW pause chỉ còn giới hạn ở các giai đoạn Initial Mark và Final Mark, thường rất ngắn. Điều này làm cho CMS trở thành một lựa chọn phù hợp cho các ứng dụng yêu cầu low-latency. Bên cạnh đó, CMS cũng xử lý tốt các Circular References.

Tuy nhiên, CMS cũng có những nhược điểm và thách thức nhất định. Overhead của Write Barrier là một chi phí cố hữu. Trong các workload có tần suất ghi con trỏ cao, overhead này có thể trở nên đáng kể. Một hiện tượng khác là Floating Garbage: một object có thể trở thành garbage sau khi nó đã được GC tô màu Black trong chu kỳ marking hiện tại. Object này sẽ không được thu hồi cho đến chu kỳ GC tiếp theo, điều này có thể làm tăng một chút memory footprint. CMS, ít nhất là trong các phiên bản không thực hiện compacting như của Go GC, vẫn phải đối mặt với vấn đề Fragmentation. Mặc dù không gây ra STW pause dài, các GC worker vẫn tiêu tốn tài nguyên CPU khi chạy đồng thời với ứng dụng. Cuối cùng, việc triển khai một thuật toán concurrent GC đúng đắn, hiệu quả và không có race condition là một nhiệm vụ kỹ thuật cực kỳ phức tạp.

8. Các Hướng Tiếp Cận Khác Trong Việc Thiết Kế GC Độ Trễ Thấp

Bên cạnh CMS, cộng đồng nghiên cứu và phát triển runtime liên tục khám phá các giải pháp khác nhằm đạt được GC với độ trễ thấp và hiệu năng cao.

Generational GC dựa trên một quan sát thực nghiệm gọi là Weak Generational Hypothesis, cho rằng hầu hết các objects có vòng đời rất ngắn, trong khi một số ít objects sống sót qua giai đoạn đầu thì có xu hướng sống rất lâu. Dựa trên giả thuyết này, heap được chia thành các "thế hệ", thường là Young Generation (bao gồm Eden space và hai Survivor spaces) và Old Generation (hay Tenured space). Các objects mới thường được cấp phát trong Young Generation. GC ở Young Generation, thường được gọi là Minor GC, diễn ra thường xuyên và nhanh chóng, thường sử dụng thuật toán Copying GC. Objects nào sống sót qua nhiều chu kỳ Minor GC sẽ được "promoted" lên Old Generation. GC ở Old Generation, gọi là Major GC hoặc Full GC, diễn ra ít thường xuyên hơn. Ưu điểm của Generational GC là các Minor GC thường rất nhanh. Tuy nhiên, chúng yêu cầu một loại write barrier đặc biệt để theo dõi con trỏ từ Old Generation trỏ đến các objects trong Young Generation. Generational GC rất phổ biến trong các runtime như JVM (ví dụ: G1 GC, Parallel GC) và .NET GC.

Một hướng phát triển khác là các Mostly/Fully Concurrent & Compacting Collectors. Đây là những thuật toán GC tiên tiến nhất, cố gắng thực hiện cả quá trình marking và compacting một cách đồng thời hoặc với STW pause cực kỳ ngắn. Các ví dụ điển hình bao gồm ZGC và Shenandoah GC trong JVM. Chúng sử dụng các kỹ thuật rất phức tạp như colored pointers, load barriers, và concurrent compaction. Tuy nhiên, chúng thường có overhead về throughput cao hơn một chút.

9. Sự Lựa Chọn Cơ Chế GC Trong Ngôn Ngữ Lập Trình: Phản Ánh Triết Lý Thiết Kế

Việc một ngôn ngữ lập trình lựa chọn sử dụng loại GC nào, hoặc thậm chí là không sử dụng GC, thường phản ánh triết lý thiết kế và mục tiêu chính của ngôn ngữ đó.

Go ưu tiên sự đơn giản của runtime và công cụ phát triển. Go cung cấp một GC duy nhất, dựa trên Concurrent Mark-Sweep, và liên tục tối ưu hóa nó cho mục tiêu chính là low-latency and predictable (độ trễ thấp và có thể dự đoán) cho các ứng dụng mạng.

Ngược lại, Java Virtual Machine (JVM) cung cấp một loạt các lựa chọn GC (như G1, ZGC, Shenandoah). Điều này cho phép người dùng tinh chỉnh GC cho các loại workload rất khác nhau, đổi lại bằng sự phức tạp trong lựa chọn và tuning.

C# và .NET runtime sử dụng một generational GC hiệu quả. Python, trong CPython implementation, sử dụng một cơ chế kết hợp giữa Reference Counting và một tracing GC theo chu kỳ.

Trong khi đó, ngôn ngữ Rust chọn một hướng đi hoàn toàn khác: không sử dụng runtime GC. Thay vào đó, Rust đảm bảo an toàn bộ nhớ tại compile-time thông qua một hệ thống ownership, borrowing, và lifetimes.

Thu hồi bộ nhớ tự động là một lĩnh vực nghiên cứu và phát triển không ngừng trong khoa học máy tính. Qua nhiều năm làm việc, tôi nhận thấy không tồn tại một thuật toán GC "hoàn hảo" nào phù hợp cho mọi tình huống sử dụng; mỗi lựa chọn đều là một sự đánh đổi giữa các yếu tố như pause time, throughput, memory footprint, và độ phức tạp. Thuật toán Concurrent Mark-Sweep, dù có những cải tiến đáng kể, vẫn có những hạn chế riêng mà tôi đã gặp phải trong các dự án thực tế.

Qua kinh nghiệm của bản thân, tôi thấy rằng việc hiểu biết sâu sắc về cơ chế hoạt động của hệ thống GC trong ngôn ngữ lập trình là yếu tố then chốt để phát triển phần mềm hiệu năng cao. Nhiều vấn đề tôi gặp phải trong quá khứ đã được giải quyết khi tôi dành thời gian nghiên cứu kỹ hơn về cách GC hoạt động trong môi trường cụ thể.

Việc sử dụng các công cụ profiling và tracing để phân tích hành vi của GC không chỉ là lý thuyết suông mà là thực hành cần thiết mà dev nên áp dụng thường xuyên. Garbage Collection vẫn đang tiếp tục phát triển, việc không ngừng học hỏi và áp dụng những hiểu biết này vào thực tế là điều cần thiết cho mọi dev, đặc biệt những ai muốn khác biệt so với những thanh niên nửa vời khác 🤌