+1

Reset nhật ký tự học BE: Ngày 1 - Garbage Collector trong Flutter/Dart & Go

Bình Phước, 6/4/2025

NÍ HẢO AE, 2 tuần trước là thật bận rộn vì mình phải chuyển trọ nên bỏ bê chuyện tự học. An cư lập nghiệp mà, phải ổn định chỗ ở thì đầu óc mới tập trung làm việc được. Ước gì có tiền mua hẳn 1 cái nhà dưới quê, về sống với bố mẹ, khỏi lo chuyển trọ nữa. Hehe. Thôi tính reset lại nhật ký he.



I - Garbage là gì?

Garbage là 1 vùng nhớ được khởi tạo nhưng không có con trỏ nào trỏ tới nó. Garbage là một vùng nhớ vô dụng, không được sử dụng. Dần dần nó sẽ dẫn đến hiện tượng memory leak.

Ví dụ với C++:

void someFunc() {
    int* ptr = new int(10); // Tạo một vùng nhớ mới, biến ptr sẽ trỏ vào vùng nhớ đó
    ptr = null; // mất tham chiếu từ ptr, tạo ra một garbage
}

Ví dụ với Dart:

void someFunc() {
  var obj = Object();
  obj = null; // Giờ Object là 1 garbage vì obj đã mất tham chiếu đến nó.
}

Ví dụ với Go:

func someFunc() {
    ptr := new(int)
    *ptr = 10
    ptr = null // mất tham chiếu từ ptr, tạo ra một garbage
}


II - Garbage collector (viết tắt: GC)

Quản lý pointer trong C++

Trong C++ không có khái niệm garbage chính thức cũng như built-in GC bởi vì ta phải tự tay quản lý bộ nhớ cho các pointer. Ví dụ:

void someFunc() {
    int* ptr = new int(10); // Tạo một vùng nhớ mới, biến ptr sẽ trỏ vào vùng nhớ đó
    delete ptr; // Giải phóng bộ nhớ
    ptr = null; // Đặt về nullptr để tránh dangling pointer
}

Đây là cách cơ bản nhất để quản lý bộ nhớ trong C++. Nội dung cũng khá đơn giản:

  • new thì phải có delete tương ứng.
  • delete xong thì phải đặt pointer về null để tránh trường hợp dangling pointer (vì bài viết này đang tập trung vào garbage nên mình xin skip phần dangling này nhé).

Các ngôn ngữ lập trình cấp cao hơn đều được tích hợp sẵn trình thu gom rác (built-in GC) và 99.99% công việc thường ngày của chúng ta sẽ chẳng cần quan tâm rằng GC là gì và nó sẽ hoạt động như thế nào. Tuy nhiên, nếu bạn có hứng thú với những thứ "under the hood", bạn nên tìm hiểu sâu hơn vì chúng rất thú vị đấy! Có khi còn pass được 1 câu hỏi phỏng vấn nữa chứ!


GC thường hoạt động như thế nào?

1. Nguyên lý cơ bản

GC hoạt động dựa trên 2 việc (nghe có vẻ là đơn giản) như sau:

  • Bước 1. Xác định garbage.
  • Bước 2. Thu hồi bộ nhớ (gom rác).

2. Các thuật toán GC phổ biến

Mark & Sweep:

  • Mark: Bắt đầu từ root (như các biến global, trong stack memory, thread...), duyệt đồ thị tới tất cả mọi nơi. Khi duyệt đồ thị, những object nào có đường đi tới thì được đánh dấu là còn sống.
  • Sweep: Duyệt qua toàn bộ heap memory, những object nào không được đánh giá còn sống, nghĩa là đó là garbage và cần phải bị loại bỏ.
  • Mark & Sweep rất cơ bản & dễ triển khai. Tuy nhiên, như mô tả phía trên, Mark & Sweep tuy đơn giản nhưng sẽ có thể gây chậm chạp. Nó phù hợp hơn với các hệ thống server.

Generational GC:

  • Dựa trên ý tưởng rằng, một số đối tượng sẽ có vòng đời ngắn (short-lived), một số đối tượng sẽ sống lâu hơn (long-lived).
  • Generational GC sẽ chia heap ra làm 2 vùng:
    • Young generation: Chứa các đối tượng mới được khởi tạo.
    • Old generation: Chứa các đối tượng đã nhiều lần sống sót qua nhiều làm gom rác.
  • GC sẽ chủ yếu quét & gom rác trong Young generation, vì nơi đây tạo ra nhiều rác nhất, chi phí xử lý cũng nhanh & ít ảnh hưởng hiệu năng. Flutter/Dart cũng sử dụng kiểu GC này.

Mark & Sweep là một cách thực thi rất cơ bản, trong khi ý tưởng của Generational GC là một trong những cách phổ biến nhất & tốt nhất hiện nay. Ngoài ra còn một số kiểu gom rác khác như Reference Counting nhưng mình không code ngôn ngữ nào xài nó cả và có vẻ nó cũng không tốt bằng Generational nên thôi skip nhá 😃.



III - GC trong Dart

Dart dùng kỹ thuật Generational GC. Bạn có thể đọc thêm về ý tưởng của Generational GC ở phía trên.

Trong quá trình hoạt động, Flutter luôn tạo ra rất nhiều widget + state nên sẽ gây ra rât nhiều garbage. Việc gom rác nhanh chóng và thường xuyên ở Young generation, kết hợp với một số kỹ thuật concurrencyschedule đúng cách sẽ giúp Flutter app luôn mượt mà và không bị drop FPS.

1. Đối với Young generation

Ở Young generation, Dart sử dụng kỹ thuật gọi là Semi-space GC. Heap sẽ được chia làm 2 nửa gọi là From-spaceTo-space. Bạn hãy quan sát hình bên dưới.

  • Khi một object mới xuất hiện, nó sẽ được khởi tạo trong vùng nhớ nằm ở From-space (2).
  • Khi nửa From-space đầy (3), GC sẽ kiểm tra xem những object nào còn sống (4), và di chuyển chúng tới nửa To-space (5).
  • Xoá sạch những garbage còn tồn tại trong From-space, và đổi vai trò của 2 nửa cho nhau (6). image.png Source ảnh: Flutter: Don’t Fear the Garbage Collector

2. Đối với Old generation

Sử dụng Mark-Sweep hoặc các biến thể của nó như Mark-Compact để tránh phân mảnh.

IV - GC trong Go

GC trong Go không sử dụng phong cách phức tạp như Generational ở Flutter/Dart. Go được sử dụng trong các ứng dụng server, và người ta muốn dễ dàng dự đoán hiệu suất của Go nên họ ưu tiên phong cách nào đấy ổn định hơn là Generational - khi ta phải implement thuật toán phức tạp cho Young Generation. Mặt khác, dù sao thì ở trong Old Generation, ta vẫn phải mark-sweep thêm lần nữa. Generational GC chỉ phù hợp với các ứng dụng mobile/web mà object bị tạo mới liên tục trong từng frame khung hình như Flutter/Dart thôi.

Mark-sweep trong Go được chạy đồng thời (concurrent) cùng với chương trình chính. Điều này giúp tránh hiện tượng "Stop-the-world" như mark-sweep truyền thống và giảm độ trễ. Còn chi tiết concurrent nó hoạt động như nào thì mình ko biết 😂.

Go sử dụng một thuật toán điều chỉnh nhịp độ (Pace algorithm) để quyết định khi nào GC được chạy. Thay vì chạy GC theo lịch cố định hoặc khi hết bộ nhớ như cách truyền thống, Pace algo dựa trên thông tin từ các chu kỳ GC trước và trạng thái hiện tại của chương trình. Pace algo giúp điều chỉnh sự cân bằng giữa RAM và CPU:

  • Nếu GC chạy quá thường xuyên --> tốn CPU.
  • Nếu GC lâu lâu mới chạy --> Tốn bộ nhớ, tốn RAM.

Pace algo dựa trên một số yếu tố sau:

  • Biến GOGC mà dev có thể điều chỉnh được, cho phép điều chỉnh tần suất gom rác của chương trình. GOGC là 1 tham số để tính Heap Goal, GOGC càng bé thì Heap Goal càng bé, tần suất gom rác càng thường xuyên hơn (và ngược lại).
  • Live Heap Size: Kích thước các object còn sống trong chu kỳ GC trước. Công thức đơn giản: Heap Goal = Live Heap Size * (1 + GOGC/100)
  • Tốc độ cấp phát bộ nhớ mới.
  • Overhead: Nếu GC chạy tốn quá nhiều CPU, nó sẽ giảm tải tần suất lại.

Tham khảo

(1) Matt Sullivan. Flutter: Don’t Fear the Garbage Collector. URL: https://medium.com/flutter/flutter-dont-fear-the-garbage-collector-d69b3ff1ca30. Last accessed: 6/4/2025.

(2) Nima Farzin. Garbage Collection in Dart and Its Implications in Flutter. URL: https://nimafarzin-pr.medium.com/garbage-collection-in-dart-and-its-implications-in-flutter-aef9de51bfc4. Last accessed: 1/4/2025.

(3) AI Chatbots.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí