Garbage Collector (GC) trong C#

Garbage Collector (GC - Trình thu gom rác) của .NET quản lý việc cấp phát và giải phóng bộ nhớ cho ứng dụng của bạn.

Mỗi khi bạn tạo một đối tượng mới, Common Language Runtime (CLR - trình thực thi ngôn ngữ chung) sẽ phân bổ bộ nhớ cho đối tượng vào heap được quản lý. Miễn là không gian địa chỉ có sẵn trong heap được quản lý, CLR tiếp tục cấp phát không gian cho các đối tượng mới.

Tuy nhiên, bộ nhớ không phải là vô hạn. Cuối cùng trình thu gom rác phải thực hiện thu thập để giải phóng một số bộ nhớ. Công cụ tối ưu hóa của trình thu gom rác xác định thời gian tốt nhất để thực hiện thu gom, dựa trên việc phân bổ đang được thực hiện.

Khi trình thu gom rác thực hiện thu thập, nó sẽ kiểm tra các đối tượng trong heap được quản lý còn được ứng dụng sử dụng không và thực hiện các hoạt động cần thiết để lấy lại bộ nhớ của chúng.

Các khái niệm cơ bản về thu gom rác

Trong CLR, trình thu gom rác (GC) đóng vai trò là trình quản lý bộ nhớ tự động. Trình thu gom rác quản lý việc cấp phát và giải phóng bộ nhớ cho một ứng dụng.

Đối với các nhà phát triển làm việc với mã được quản lý, điều này có nghĩa là bạn không phải viết mã để thực hiện các tác vụ quản lý bộ nhớ.

Quản lý bộ nhớ tự động có thể loại bỏ các vấn đề phổ biến, chẳng hạn như quên giải phóng một đối tượng và gây rò rỉ bộ nhớ hoặc cố gắng truy cập bộ nhớ cho một đối tượng đã được giải phóng.

Những lợi ích của trình thu gom rác

Trình thu gom rác cung cấp những lợi ích sau:

  • Giải phóng các nhà phát triển khỏi việc phải giải phóng bộ nhớ theo cách thủ công.
  • Phân bổ các đối tượng trên heap được quản lý một cách hiệu quả.
  • Thu hồi các đối tượng không còn được sử dụng, xóa bộ nhớ của chúng và giữ cho bộ nhớ có sẵn để phân bổ trong tương lai. Các đối tượng được quản lý sẽ tự động lấy nội dung rõ ràng để bắt đầu, vì vậy các hàm khởi tạo của chúng không phải khởi tạo mọi trường dữ liệu.
  • Cung cấp sự an toàn cho bộ nhớ bằng cách đảm bảo rằng một đối tượng không thể sử dụng nội dung của đối tượng khác.

Các khái niệm cơ bản của bộ nhớ

Danh sách sau đây tóm tắt các khái niệm bộ nhớ CLR quan trọng.

  • Mỗi tiến trình có không gian địa chỉ ảo riêng biệt. Tất cả các quy trình trên cùng một máy tính chia sẻ cùng một bộ nhớ vật lý và page file, nếu có.
  • Theo mặc định, trên máy tính 32 bit, mỗi tiến trình có 2 GB không gian địa chỉ ảo chế độ người dùng.
  • Là một nhà phát triển ứng dụng, bạn chỉ làm việc với không gian địa chỉ ảo và không bao giờ thao tác trực tiếp với bộ nhớ vật lý. Trình thu gom rác phân bổ và giải phóng bộ nhớ ảo cho bạn trên heap được quản lý. Nếu bạn đang viết mã máy, bạn sử dụng các chức năng của Windows để làm việc với không gian địa chỉ ảo. Các chức năng này phân bổ và giải phóng bộ nhớ ảo cho bạn trên các heap riêng.
  • Bộ nhớ ảo có thể ở ba trạng thái:
Trạng thái Mô tả
Free Khối bộ nhớ không có tham chiếu đến nó và có sẵn để cấp phát.
Reserved Khối bộ nhớ có sẵn để bạn sử dụng và không thể được sử dụng cho bất kỳ yêu cầu cấp phát nào khác. Tuy nhiên, bạn không thể lưu trữ dữ liệu vào khối bộ nhớ này cho đến khi nó được commit.
Committed Khối bộ nhớ được gán cho bộ nhớ vật lý.
  • Không gian địa chỉ ảo có thể bị phân mảnh. Điều này có nghĩa là có các khối trống, còn được gọi là lỗ, trong không gian địa chỉ. Khi yêu cầu cấp phát bộ nhớ ảo, trình quản lý bộ nhớ ảo phải tìm một khối trống duy nhất đủ lớn để đáp ứng yêu cầu cấp phát đó. Ngay cả khi bạn có 2 GB dung lượng trống, phân bổ yêu cầu 2 GB sẽ không thành công trừ khi tất cả dung lượng trống đó nằm trong một khối địa chỉ duy nhất.
  • Bạn có thể hết bộ nhớ nếu không có đủ không gian địa chỉ ảo để dự trữ hoặc không gian vật lý để commit. Page file được sử dụng ngay cả khi áp lực bộ nhớ vật lý (nghĩa là nhu cầu bộ nhớ vật lý) thấp. Lần đầu tiên khi áp lực bộ nhớ vật lý cao, hệ điều hành phải dành chỗ trong bộ nhớ vật lý để lưu trữ dữ liệu và nó sao lưu một số dữ liệu trong bộ nhớ vật lý vào page file. Dữ liệu đó không được phân trang cho đến khi cần thiết, vì vậy có thể gặp phải tình trạng phân trang trong các tình huống áp lực bộ nhớ vật lý thấp.

Cấp phát bộ nhớ

Khi bạn khởi tạo một tiến trình mới, runtime sẽ dành một vùng không gian địa chỉ liền kề cho tiến trình. Vùng địa chỉ dành riêng này được gọi là heap được quản lý.

Heap được quản lý duy trì một con trỏ đến địa chỉ nơi đối tượng tiếp theo trong heap sẽ được cấp phát. Ban đầu, con trỏ này được thiết lập tới địa chỉ cơ sở của heap được quản lý.

Tất cả các kiểu tham chiếu được phân bổ trên heap được quản lý. Khi một ứng dụng tạo kiểu tham chiếu đầu tiên, bộ nhớ được cấp phát cho kiểu tại địa chỉ cơ sở của heap được quản lý.

Khi ứng dụng tạo đối tượng tiếp theo, trình thu gom rác sẽ phân bổ bộ nhớ cho nó trong không gian địa chỉ ngay sau đối tượng đầu tiên. Miễn là có không gian địa chỉ, trình thu gom rác tiếp tục phân bổ không gian cho các đối tượng mới theo cách này.

Phân bổ bộ nhớ từ heap được quản lý nhanh hơn phân bổ bộ nhớ không được quản lý. Bởi vì runtime phân bổ bộ nhớ cho một đối tượng bằng cách thêm một giá trị vào một con trỏ, nó nhanh gần như cấp phát bộ nhớ từ stack.

Ngoài ra, vì các đối tượng mới được cấp phát liên tiếp được lưu trữ liền kề trong heap được quản lý, một ứng dụng có thể truy cập các đối tượng một cách nhanh chóng.

Giải phóng bộ nhớ

Công cụ tối ưu hóa của trình thu gom rác xác định thời gian tốt nhất để thực hiện thu gom dựa trên các phân bổ đang được thực hiện. Khi trình thu gom rác thực hiện thu gom, nó sẽ giải phóng bộ nhớ cho các đối tượng không còn được ứng dụng sử dụng.

Nó xác định đối tượng nào không còn được sử dụng bằng cách kiểm tra gốc của ứng dụng . Gốc của một ứng dụng bao gồm các trường tĩnh, các biến cục bộ trên ngăn xếp của một luồng, các thanh ghi CPU, các bộ xử lý GC và hàng đợi hoàn thiện.

Mỗi gốc hoặc tham chiếu đến một đối tượng trên heap được quản lý hoặc được đặt thành null. Trình thu gom rác có thể hỏi runtime cho các gốc này. Sử dụng danh sách này, trình thu gom rác tạo ra một biểu đồ chứa tất cả các đối tượng có thể truy cập được từ gốc.

Các đối tượng không có trong biểu đồ không thể truy cập được từ gốc của ứng dụng. Trình thu gom rác coi các đối tượng không thể truy cập là rác và giải phóng bộ nhớ được cấp phát cho chúng.

Trong quá trình thu thập, trình thu gom rác kiểm tra heap được quản lý, tìm kiếm các khối không gian địa chỉ bị chiếm bởi các đối tượng không thể truy cập. Khi phát hiện ra một đối tượng không thể truy cập, nó sử dụng chức năng sao chép bộ nhớ để thu gọn các đối tượng có thể truy cập trong bộ nhớ, giải phóng các khối không gian địa chỉ được phân bổ cho các đối tượng không thể truy cập.

Khi bộ nhớ cho các đối tượng có thể truy cập đã được thu gọn, trình thu gom rác thực hiện các chỉnh sửa con trỏ cần thiết để gốc của ứng dụng trỏ đến các đối tượng ở vị trí mới của chúng. Nó cũng định vị con trỏ của heap được quản lý sau đối tượng có thể truy cập cuối cùng.

Bộ nhớ chỉ bị nén nếu phát hiện một tập hợp có một số lượng đáng kể các đối tượng không thể truy cập. Nếu tất cả các đối tượng trong heap được quản lý tồn tại trong một tập hợp, thì không cần nén bộ nhớ.

Để cải thiện hiệu suất, runtime phân bổ bộ nhớ cho các đối tượng lớn trong một heap riêng biệt. Bộ thu gom rác tự động giải phóng bộ nhớ cho các đối tượng lớn. Tuy nhiên, để tránh di chuyển các đối tượng lớn trong bộ nhớ, bộ nhớ này thường không được nén.

Điều kiện thu gom rác

Việc thu gom rác xảy ra khi một trong các điều kiện sau là đúng:

  • Hệ thống có bộ nhớ vật lý thấp. Điều này được phát hiện bởi thông báo bộ nhớ thấp từ Hệ điều hành hoặc bộ nhớ thấp được chỉ ra bởi máy chủ.
  • Bộ nhớ được sử dụng bởi các đối tượng được phân bổ trên heap được quản lý vượt qua ngưỡng có thể chấp nhận được. Ngưỡng này liên tục được điều chỉnh trong quá trình chạy.
  • Phương thức GC.Collect được gọi. Trong hầu hết các trường hợp, bạn không phải gọi phương thức này, bởi vì trình thu gom rác chạy liên tục. Phương thức này chủ yếu được sử dụng cho các tình huống thử nghiệm.

Heap được quản lý

Sau khi trình thu gom rác được khởi tạo bởi CLR, nó sẽ phân bổ một đoạn bộ nhớ để lưu trữ và quản lý các đối tượng. Bộ nhớ này được gọi là heap được quản lý, trái ngược với heap gốc trong hệ điều hành.

Có một heap được quản lý cho mỗi tiến trình được quản lý. Tất cả các luồng trong tiến trình cấp phát bộ nhớ cho các đối tượng trên cùng một heap.

Để dự trữ bộ nhớ, trình thu gom rác gọi chức năng Windows VirtualAlloc và dự trữ một phân đoạn bộ nhớ tại một thời điểm cho các ứng dụng được quản lý. Trình thu gom rác cũng dự trữ các phân đoạn, nếu cần và giải phóng các phân đoạn trở lại hệ điều hành (sau khi xóa chúng khỏi bất kỳ đối tượng nào) bằng cách gọi hàm Windows VirtualFree.

Quan trọng: Kích thước của các phân đoạn được phân bổ bởi trình thu gom rác là dành riêng cho việc triển khai và có thể thay đổi bất kỳ lúc nào, kể cả trong các bản cập nhật định kỳ. Ứng dụng của bạn không bao giờ được đưa ra các giả định về hoặc phụ thuộc vào một kích thước phân đoạn cụ thể, cũng như không nên cố gắng cấu hình lượng bộ nhớ có sẵn để phân bổ phân đoạn.

Càng ít đối tượng được phân bổ trên heap, thì trình thu gom rác càng phải làm ít công việc hơn. Khi bạn phân bổ các đối tượng, không sử dụng các giá trị vượt quá nhu cầu của bạn, chẳng hạn như phân bổ một mảng 32 byte khi bạn chỉ cần 15 byte.

Khi một trình thu gom rác được kích hoạt, nó sẽ lấy lại bộ nhớ bị chiếm bởi các đối tượng không còn được sử dụng. Quá trình xử lý sẽ thu gọn các đối tượng còn sử dụng lại cùng nhau, và không gian chết được loại bỏ, do đó làm cho heap nhỏ hơn. Điều này đảm bảo rằng các đối tượng được phân bổ cùng nhau sẽ ở cùng nhau trên heap được quản lý.

Khả năng hoạt động (tần suất và thời gian) của các trình thu gom rác là kết quả của khối lượng phân bổ và lượng bộ nhớ còn sót lại trên heap được quản lý.

Heap có thể được coi là sự tích tụ của hai heap: heap đối tượng lớn và heap đối tượng nhỏ. Heap đối tượng lớn chứa các đối tượng có kích thước từ 85.000 byte trở lên, thường là các mảng. Rất hiếm khi một đối tượng có kích thước lớn như vậy.

Các thế hệ (generation)

Thuật toán GC dựa trên một số cân nhắc:

  • Việc thu gọn bộ nhớ cho một phần của heap được quản lý sẽ nhanh hơn so với toàn bộ heap được quản lý.
  • Các đối tượng mới hơn có tuổi thọ ngắn hơn và các đối tượng cũ hơn có tuổi thọ dài hơn.
  • Các đối tượng mới hơn có xu hướng liên quan đến nhau và được ứng dụng truy cập vào cùng một thời điểm.

Việc thu gom rác chủ yếu xảy ra với việc xử lý các đối tượng tồn tại trong thời gian ngắn. Để tối ưu hóa hiệu suất của trình thu gom rác, heap được quản lý được chia thành ba thế hệ 0, 1 và 2, vì vậy nó có thể xử lý riêng biệt các đối tượng tồn tại lâu dài và ngắn hạn.

Trình thu gom rác lưu trữ các đối tượng mới trong thế hệ 0 (gen 0). Các đối tượng được tạo sớm trong vòng đời của ứng dụng mà vẫn tồn tại sau những lần thu gom rác sẽ được lưu trữ trong thế hệ 1 và 2.

Bởi vì việc thu gọn một phần của heap được quản lý nhanh hơn so với toàn bộ đống, chiến lược này cho phép trình thu gom rác giải phóng bộ nhớ trong một thế hệ cụ thể thay vì giải phóng bộ nhớ cho toàn bộ heap được quản lý mỗi khi nó thực hiện thu gom rác.

Thế hệ 0 (gen 0)

Đây là thế hệ trẻ nhất và chứa các vật thể tồn tại trong thời gian ngắn. Một ví dụ về một đối tượng tồn tại trong thời gian ngắn là một biến tạm thời. Việc thu gom rác xảy ra thường xuyên nhất trong thế hệ này.

Các đối tượng mới được cấp phát tạo thành một thế hệ đối tượng mới và mặc nhiên là các tập hợp thế hệ 0. Tuy nhiên, nếu chúng là các vật thể lớn, chúng đi trên đống vật thể lớn (LOH), đôi khi được gọi là thế hệ 3 . Thế hệ 3 là một thế hệ vật lý được thu thập một cách hợp lý như một phần của thế hệ 2.

Hầu hết các đối tượng được lấy lại để thu gom rác trong thế hệ 0 và không tồn tại cho thế hệ tiếp theo.

Nếu một ứng dụng cố gắng tạo một đối tượng mới khi thế hệ 0 đã đầy, trình thu gom rác sẽ thực hiện thu gom rác nhằm giải phóng không gian địa chỉ cho đối tượng.

Trình thu gom rác bắt đầu bằng cách kiểm tra các đối tượng trong thế hệ 0 thay vì tất cả các đối tượng trong heap được quản lý. Chỉ cần thực hiện thu gom rác của thế hệ 0 thường sẽ lấy lại đủ bộ nhớ để cho phép ứng dụng tiếp tục tạo các đối tượng mới.

Thế hệ 1 (gen 1)

Thế hệ này chứa các đối tượng tồn tại trong thời gian ngắn và đóng vai trò như một bộ đệm giữa các đối tượng tồn tại trong thời gian ngắn và các đối tượng tồn tại lâu dài.

Sau khi trình thu gom rác thực hiện thu gom rác ở thế hệ 0, nó sẽ nén bộ nhớ cho các đối tượng có thể truy cập và chuyển chúng sang thế hệ 1. Bởi vì các đối tượng tồn tại này có xu hướng có tuổi thọ lâu hơn, nên việc đưa chúng lên thế hệ cao hơn là rất hợp lý. Trình thu gom rác không phải kiểm tra lại các đối tượng trong thế hệ 1 và 2 mỗi khi nó thực hiện thu gom rác ở thế hệ 0.

Nếu việc thu gom rác ở thế hệ 0 không lấy đủ bộ nhớ để ứng dụng tạo một đối tượng mới, bộ thu gom rác có thể thực hiện thu gom rác ở thế hệ 1, sau đó thế hệ 2. Các đối tượng trong thế hệ 1 tồn tại sau khi thực hiện thu gom rác sẽ được thăng cấp lên thế hệ 2.

Thế hệ 2 (gen 2)

Thế hệ này chứa các đối tượng tồn tại lâu dài. Ví dụ về một đối tượng tồn tại lâu dài là một đối tượng trong ứng dụng máy chủ có chứa dữ liệu tĩnh tồn tại trong suốt tiến trình.

Các đối tượng ở thế hệ 2 tồn tại cho đến khi chúng được xác định là không thể truy cập được.

Các đối tượng trên heap đối tượng lớn (đôi khi được gọi là thế hệ 3 ) cũng được thu gom rác trong thế hệ 2.

Trình thu gom rác thực hiện trên các thế hệ cụ thể khi điều kiện đảm bảo. Thu gom rác của một thế hệ có nghĩa là thu thập các đối tượng trong thế hệ đó và tất cả các thế hệ trước của nó. Trình thu gom rác thế hệ 2 còn được gọi là trình thu gom rác đầy đủ, vì nó thu hồi các đối tượng trong tất cả các thế hệ (nghĩa là tất cả các đối tượng trong heap được quản lý).

Sự tồn tại và thăng cấp

Những đối tượng không được thu hồi bởi trình thu gom rác được coi là đối tượng tồn tại và được thăng cấp lên thế hệ tiếp theo:

  • Các đối tượng tồn tại trong quá trình thu gom rác thế hệ 0 được thăng cấp lên thế hệ 1.
  • Các đối tượng tồn tại trong quá trình thu gom rác thế hệ 1 sẽ được thăng cấp lên thế hệ 2.
  • Các vật thể tồn tại trong quá trình thu gom rác thế hệ 2 vẫn còn trong thế hệ 2.

Khi trình thu gom rác phát hiện ra rằng tỷ lệ sống sót cao trong một thế hệ, nó sẽ tăng ngưỡng phân bổ cho thế hệ đó. Lần thu gom rác tiếp theo có dung lượng bộ nhớ được lấy lại đáng kể. CLR liên tục cân bằng hai ưu tiên: không để tập làm việc của ứng dụng quá lớn bằng cách trì hoãn việc thu gom rác và không để trình thu gom rác chạy quá thường xuyên.

Điều gì xảy ra trong quá trình thu gom rác

Việc thu gom rác có các giai đoạn sau:

  • Giai đoạn đánh dấu tìm và tạo danh sách tất cả các đối tượng còn sống (vẫn còn được ứng dụng sử dụng).
  • Giai đoạn định vị lại để cập nhật các tham chiếu đến các đối tượng sẽ được nén lại.
  • Giai đoạn nén để lấy lại không gian bị chiếm bởi các đối tượng đã chết (không còn được ứng dụng sử dụng) và thu gọn các đối tượng còn sống. Giai đoạn nén sẽ di chuyển các đối tượng còn sót lại trong trình thu gom rác về phần cuối cũ hơn của phân đoạn.

Bởi vì các đối tượng của thế hệ 2 có thể chiếm nhiều phân đoạn, các đối tượng được thăng cấp vào thế hệ 2 có thể được di chuyển vào một phân đoạn cũ hơn. Cả những đối tượng sống sót ở thế hệ 1 và thế hệ 2 đều có thể được chuyển sang một phân đoạn khác, vì chúng được thăng cấp lên thế hệ 2.

Thông thường, heap đối tượng lớn (LOH) không được nén lại, bởi vì sao chép các đối tượng lớn sẽ xảy ra vấn đề về hiệu suất. Tuy nhiên, trong .NET Core và .NET Framework 4.5.1 trở lên, bạn có thể sử dụng thuộc tính GCSettings.LargeObjectHeapCompactionMode để thu gọn heap đối tượng lớn theo yêu cầu. Ngoài ra, LOH tự động được nén khi giới hạn cứng được thiết lập bằng cách chỉ định:

Trình thu gom rác sử dụng thông tin sau để xác định xem các đối tượng có đang sống hay không:

  • Stack roots. Các biến ngăn xếp được cung cấp bởi trình biên dịch JIT và stack walker. Tối ưu hóa JIT có thể kéo dài hoặc rút ngắn các vùng mã trong đó các biến ngăn xếp được báo cáo cho trình thu gom rác.
  • Garbage collection handles. Xử lý trỏ đến các đối tượng được quản lý và có thể được cấp phát bằng mã người dùng hoặc CLR.
  • Static data. Các đối tượng tĩnh trong miền ứng dụng có thể tham chiếu đến các đối tượng khác. Mỗi miền ứng dụng theo dõi các đối tượng tĩnh của nó.

Trước khi bắt đầu thu gom rác, tất cả các luồng được quản lý sẽ bị tạm ngưng ngoại trừ luồng đã kích hoạt thu gom rác.

Hình minh họa sau đây cho thấy một luồng kích hoạt thu gom rác và khiến các luồng khác bị tạm ngưng.

Điều gì xảy ra trong quá trình thu gom rác

Tài nguyên không được quản lý

Đối với hầu hết các đối tượng mà ứng dụng của bạn tạo, bạn có thể dựa vào trình thu gom rác để tự động thực hiện các tác vụ quản lý bộ nhớ cần thiết.

Tuy nhiên, các tài nguyên không được quản lý yêu cầu dọn dẹp rõ ràng. Loại tài nguyên không được quản lý phổ biến nhất là một đối tượng bao bọc tài nguyên hệ điều hành, chẳng hạn như trình xử lý file, trình điều khiển cửa sổ hoặc kết nối mạng.

Mặc dù trình thu gom rác có thể theo dõi vòng đời của một đối tượng được quản lý đóng gói tài nguyên không được quản lý, nhưng nó không có kiến thức cụ thể về cách dọn dẹp tài nguyên.

Khi bạn tạo một đối tượng đóng gói tài nguyên không được quản lý, bạn nên cung cấp mã cần thiết để dọn dẹp tài nguyên không được quản lý trong một phương thức Dispose công khai. Bằng cách cung cấp một phương thức Dispose, bạn cho phép người dùng đối tượng của mình giải phóng bộ nhớ của đối tượng một cách rõ ràng khi họ kết thúc với đối tượng. Khi bạn sử dụng một đối tượng đóng gói tài nguyên không được quản lý, hãy đảm bảo gọi phương thức Dispose khi cần thiết.

Bạn cũng phải cung cấp một cách để các tài nguyên không được quản lý của bạn được giải phóng trong trường hợp khách hàng của bạn quên gọi Dispose. Bạn có thể sử dụng một xử lý an toàn để bọc tài nguyên không được quản lý hoặc ghi đè phương thức Object.Finalize().

Lập Trình C#
Bài Viết Liên Quan:
Common Language Runtime (CLR) trong C#
Trung Nguyen 29/03/2021
Common Language Runtime (CLR) trong C#

Trong bài viết này, chúng ta sẽ tìm hiểu về Common Language Runtime (CLR) là gì? Cách biên dịch và chạy ứng dụng được viết bằng C#.

Giới thiệu về .NET Framework
Trung Nguyen 29/03/2021
Giới thiệu về .NET Framework

Cung cấp cho bạn một cái nhìn tổng quan về .NET Framework, để hiểu rõ các thành phần và kiến trúc của .NET Framework.

int[] và int[,] trong C#: Ai nhanh hơn
Trung Nguyen 10/10/2020
int[] và int[,] trong C#: Ai nhanh hơn

Hiểu được sự khác biệt giữa các loại mảng trong C# sẽ giúp bạn chọn cấu trúc dữ liệu chính xác cho mọi trường hợp.

Struct và class trong C#: Ai nhanh hơn
Trung Nguyen 09/10/2020
Struct và class trong C#: Ai nhanh hơn

Trong bài viết này, tôi sẽ so sánh sự khác biệt về hiệu suất giữa struct và class trong C#: Ai nhanh hơn.