Quản Lý Bộ Nhớ Trong .NET: Garbage Collector, Finalizer và IDisposable

Quản Lý Bộ Nhớ Trong .NET: Garbage Collector, Finalizer và IDisposable

March 29, 2025 0 By Nam Vu

Bộ thu gom rác (Garbage Collection – GC) trong .NET là một quy trình phức tạp hơn nhiều người tưởng. Về cơ bản, GC quản lý việc phân bố và giải phóng bộ nhớ cho mã lệnh quản lý (managed code). Tuy nhiên, nhiều anh em developers cho rằng GC hoàn toàn nằm ngoài tầm kiểm soát, điều này không hoàn toàn chính xác.

Việc hiểu GC hoạt động như thế nào sẽ giúp bạn tối ưu hóa hiệu suất của ứng dụng. Ngoài ra, các câu hỏi về GC cũng là một trong những chủ đề phổ biến trong các buổi phỏng vấn kỹ thuật.

Ngoài GC, bạn cũng cần hiểu rõ hai cơ chế quản lý bộ nhớ quan trọng khác: FinalizeIDisposable, giúp quản lý tài nguyên chưa được GC quản lý.

1. Giới thiệu về Quản lý Bộ nhớ trong .NET

Trong môi trường .NET, bộ nhớ được chia thành hai phần chính:

  • Stack: Dùng để lưu trữ các biến giá trị (value types) và quản lý thông qua cơ chế LIFO (Last In, First Out).
  • Heap: Dùng để lưu trữ các biến tham chiếu (reference types) và được quản lý bởi Garbage Collector (GC).

Bộ nhớ Heap có hai phần chính:

  • Small Object Heap (SOH): Chứa các đối tượng có kích thước nhỏ (< 85 KB).
  • Large Object Heap (LOH): Chứa các đối tượng có kích thước lớn (≥ 85 KB).

Garbage Collector chịu trách nhiệm thu hồi bộ nhớ của các đối tượng không còn được sử dụng nhằm ngăn chặn rò rỉ bộ nhớ. Tuy nhiên, GC không tự động giải phóng tài nguyên không được quản lý (unmanaged resources) như kết nối tệp tin, kết nối cơ sở dữ liệu, socket, handle hệ thống,… Để xử lý điều này, .NET cung cấp FinalizerIDisposable.

2. Garbage Collector (GC) trong .NET

2.1 Cách thức hoạt động của GC

GC hoạt động theo cơ chế Mark-and-Sweep + Compacting:

  1. Mark: Xác định các đối tượng đang còn được tham chiếu (reachable objects).
  2. Sweep: Dọn dẹp các đối tượng không còn được tham chiếu.
  3. Compact: Gom nhóm các đối tượng còn sống lại để tối ưu bộ nhớ (áp dụng cho SOH nhưng không tự động thực hiện trên LOH).

2.2 Các điều kiện kích hoạt GC

GC được kích hoạt khi:

  • Hệ thống có ít bộ nhớ vật lý (OS gửi tín hiệu low memory).
  • Bộ nhớ được sử dụng vượt quá một ngưỡng nhất định.
  • Gọi phương thức GC.Collect() (không khuyến khích trừ trường hợp đặc biệt).

2.3 Các thế hệ (Generations) trong GC

GC chia bộ nhớ Heap thành ba thế hệ để tối ưu hiệu suất:

  • Generation 0 (Gen0): Chứa các đối tượng mới được tạo, tuổi thọ ngắn.
  • Generation 1 (Gen1): Chứa các đối tượng đã sống sót qua ít nhất một lần thu gom rác.
  • Generation 2 (Gen2): Chứa các đối tượng sống lâu dài như static objects, singleton, cache.

GC thường thu gom Gen0 trước, nếu chưa đủ bộ nhớ thì thu gom Gen1, và chỉ thu gom Gen2 khi thật sự cần thiết.

2.4 Large Object Heap (LOH) và hiệu suất

  • Các đối tượng lớn (≥ 85 KB) được đặt trong Large Object Heap (LOH).
  • LOH chỉ được thu gom khi thu gom Gen2, điều này có thể gây ảnh hưởng hiệu suất nếu không được quản lý tốt.
  • Bởi vì LOH không tự động nén bộ nhớ, nếu có nhiều khoảng trống (fragmentation), bạn cần dùng GCSettings.LargeObjectHeapCompactionMode.

2.5 Các chế độ Garbage Collection

.NET cung cấp hai chế độ chính:

  • Workstation GC: Dành cho ứng dụng desktop, chạy trên một luồng, tối ưu cho tính tương tác.
  • Server GC: Dành cho ứng dụng server, chạy song song trên nhiều luồng để tăng hiệu suất.

Ngoài ra, GC có hai chế độ nền:

  • Background GC (dành cho Gen2, giúp giảm thời gian pause).
  • Concurrent GC (cho phép GC chạy đồng thời với luồng ứng dụng).

3. Finalizer trong .NET

3.1 Finalizer là gì?

Finalizer (hay Destructor) là phương thức đặc biệt trong C# được sử dụng để dọn dẹp tài nguyên không được quản lý. Nó được tự động gọi bởi GC trước khi đối tượng bị thu gom.

class MyClass
{
    ~MyClass() // Finalizer
    {
        Console.WriteLine("Finalizer called!");
    }
}

3.2 Cách hoạt động của Finalizer

  • Khi một đối tượng có Finalizer, nó sẽ được đưa vào Finalization Queue.
  • GC sẽ xử lý Finalization Queue trước khi thu gom bộ nhớ.
  • Điều này có thể khiến đối tượng tồn tại lâu hơn so với bình thường (vì cần chờ GC chạy Finalizer).

3.3 Hạn chế của Finalizer

  • Không đảm bảo thời điểm thực thi, chỉ chạy khi GC quyết định thu gom.
  • Làm chậm quá trình thu gom rác vì đối tượng cần thêm một vòng đời thu gom.
  • Không phù hợp để giải phóng tài nguyên quan trọng như kết nối file hoặc database.

3.4 Ngăn chặn gọi Finalizer không cần thiết

Bạn có thể sử dụng GC.SuppressFinalize(this) để ngăn GC gọi Finalizer nếu tài nguyên đã được giải phóng thủ công.

csharp
CopyEdit
class MyClass : IDisposable
{
    ~MyClass()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Giải phóng tài nguyên managed
        }
        // Giải phóng tài nguyên unmanaged
    }
}

4. IDisposable và Dispose Pattern

4.1 IDisposable là gì?

IDisposable là một interface cung cấp phương thức Dispose() để giải phóng tài nguyên thủ công thay vì chờ GC.

4.2 Triển khai IDisposable

lass ResourceHolder : IDisposable
{
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Giải phóng tài nguyên managed
            }
            // Giải phóng tài nguyên unmanaged
            disposed = true;
        }
    }

    ~ResourceHolder()
    {
        Dispose(false);
    }
}

4.3 Sử dụng using để tự động gọi Dispose

Khi một lớp triển khai IDisposable, bạn có thể sử dụng khối using để tự động gọi Dispose().

using (ResourceHolder resource = new ResourceHolder())
{
    // Sử dụng resource
} // Dispose() được gọi tự động khi thoát khỏi khối using

Việc quyết định sử dụng FinalizerIDisposable phụ thuộc vào loại tài nguyên mà đối tượng quản lý.

Nếu đối tượng chỉ sử dụng các tài nguyên được quản lý (managed resources) như RAM, biến, hoặc danh sách (List, Dictionary, …), thì không cần Finalizer, mà chỉ cần triển khai IDisposable để giải phóng tài nguyên sớm hơn thay vì chờ GC.

Đối với các đối tượng sử dụng tài nguyên không được quản lý (unmanaged resources) như tệp tin, socket, handle hệ thống, hoặc kết nối cơ sở dữ liệu, thì cần kết hợp cả FinalizerIDisposable. Finalizer đảm bảo tài nguyên được giải phóng ngay cả khi lập trình viên quên gọi Dispose(), trong khi IDisposable cho phép giải phóng tài nguyên thủ công một cách hiệu quả. Cuối cùng, nếu đối tượng không sử dụng tài nguyên đặc biệt và có vòng đời ngắn, thì không cần triển khai Finalizer hay IDisposable, vì Garbage Collector sẽ tự động xử lý việc thu gom bộ nhớ.

#ntechdevelopers