Hiểu bộ sưu tập rác trong .NET


170

Hãy xem xét mã dưới đây:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Bây giờ, mặc dù biến c1 trong phương thức chính nằm ngoài phạm vi và không được tham chiếu thêm bởi bất kỳ đối tượng nào khác khi GC.Collect()được gọi, tại sao nó không được hoàn thành ở đó?


8
GC không ngay lập tức miễn phí các trường hợp khi chúng nằm ngoài phạm vi. Nó làm như vậy khi nó nghĩ rằng nó cần thiết. Bạn có thể đọc tất cả mọi thứ về GC tại đây: msdn.microsoft.com/en-US/l
Library / vstudio / 08659wtx.aspx

@ user1908061 (Pssst. Liên kết của bạn bị hỏng.)
Dragomok

Câu trả lời:


352

Bạn đang bị vấp vào đây và rút ra kết luận rất sai vì bạn đang sử dụng trình gỡ lỗi. Bạn sẽ cần chạy mã của mình theo cách nó chạy trên máy của người dùng. Trước tiên, chuyển sang bản dựng Bản phát hành với Trình quản lý cấu hình + Xây dựng, thay đổi kết hợp "Cấu hình giải pháp hoạt động" ở góc trên bên trái thành "Phát hành". Tiếp theo, đi vào Công cụ + Tùy chọn, Gỡ lỗi, Chung và bỏ chọn tùy chọn "Bỏ tối ưu hóa JIT".

Bây giờ chạy lại chương trình của bạn và sửa lại mã nguồn. Lưu ý cách niềng răng thêm không có tác dụng gì cả. Và lưu ý cách đặt biến thành null không có sự khác biệt nào cả. Nó sẽ luôn in "1". Bây giờ nó hoạt động theo cách bạn hy vọng và mong đợi nó sẽ hoạt động.

Điều này không có nhiệm vụ giải thích lý do tại sao nó hoạt động rất khác nhau khi bạn chạy bản dựng Debug. Điều đó đòi hỏi phải giải thích làm thế nào trình thu gom rác phát hiện ra các biến cục bộ và mức độ ảnh hưởng của nó khi có trình gỡ lỗi.

Trước hết, jitter thực hiện hai nhiệm vụ quan trọng khi nó biên dịch IL cho một phương thức thành mã máy. Cái đầu tiên rất dễ thấy trong trình gỡ lỗi, bạn có thể thấy mã máy với cửa sổ Gỡ lỗi + Windows + Tháo gỡ. Nhiệm vụ thứ hai tuy nhiên hoàn toàn vô hình. Nó cũng tạo ra một bảng mô tả cách các biến cục bộ bên trong thân phương thức được sử dụng. Bảng đó có một mục nhập cho mỗi đối số phương thức và biến cục bộ có hai địa chỉ. Địa chỉ nơi đầu tiên biến sẽ lưu trữ một tham chiếu đối tượng. Và địa chỉ của lệnh mã máy nơi biến đó không còn được sử dụng. Ngoài ra, cho dù biến đó được lưu trữ trên khung ngăn xếp hoặc thanh ghi cpu.

Bảng này rất cần thiết cho trình thu gom rác, nó cần biết nơi để tìm các tham chiếu đối tượng khi nó thực hiện một bộ sưu tập. Khá dễ thực hiện khi tham chiếu là một phần của một đối tượng trên heap GC. Chắc chắn không dễ thực hiện khi tham chiếu đối tượng được lưu trong thanh ghi CPU. Bảng nói nơi để tìm.

Địa chỉ "không còn được sử dụng" trong bảng là rất quan trọng. Nó làm cho người thu gom rác rất hiệu quả . Nó có thể thu thập một tham chiếu đối tượng, ngay cả khi nó được sử dụng bên trong một phương thức và phương thức đó chưa hoàn thành việc thực thi. Điều này rất phổ biến, ví dụ phương thức Main () của bạn sẽ chỉ dừng thực thi ngay trước khi chương trình của bạn kết thúc. Rõ ràng bạn sẽ không muốn bất kỳ tham chiếu đối tượng nào được sử dụng bên trong phương thức Main () đó tồn tại trong suốt thời gian của chương trình, điều đó sẽ dẫn đến rò rỉ. Jitter có thể sử dụng bảng để phát hiện ra rằng một biến cục bộ như vậy không còn hữu ích nữa, tùy thuộc vào mức độ chương trình đã tiến triển bên trong phương thức Main () đó trước khi thực hiện cuộc gọi.

Một phương thức gần như kỳ diệu có liên quan đến bảng đó là GC.KeepAlive (). Đây là một phương pháp rất đặc biệt, nó không tạo ra bất kỳ mã nào cả. Nhiệm vụ duy nhất của nó là sửa đổi bảng đó. Nó kéo dàivòng đời của biến cục bộ, ngăn chặn tham chiếu mà nó lưu trữ khỏi việc thu gom rác. Lần duy nhất bạn cần sử dụng là để ngăn chặn GC quá háo hức với việc thu thập một tham chiếu, điều này có thể xảy ra trong các kịch bản xen kẽ trong đó một tham chiếu được chuyển đến mã không được quản lý. Trình thu gom rác không thể thấy các tham chiếu như vậy đang được sử dụng bởi mã đó vì nó không được biên dịch bởi jitter nên không có bảng cho biết nơi cần tìm tham chiếu. Truyền một đối tượng ủy nhiệm cho một hàm không được quản lý như EnumWindows () là ví dụ về bản tóm tắt khi bạn cần sử dụng GC.KeepAlive ().

Vì vậy, như bạn có thể biết từ đoạn mã mẫu của mình sau khi chạy nó trong bản dựng Phát hành, các biến cục bộ có thể được thu thập sớm, trước khi phương thức thực hiện xong. Thậm chí mạnh mẽ hơn, một đối tượng có thể được thu thập trong khi một trong các phương thức của nó chạy nếu phương thức đó không còn đề cập đến điều này nữa. Có một vấn đề với điều đó, rất khó xử khi gỡ lỗi một phương thức như vậy. Vì bạn cũng có thể đặt biến trong cửa sổ Watch hoặc kiểm tra nó. Và nó sẽ biến mất trong khi bạn gỡ lỗi nếu xảy ra GC. Điều đó sẽ rất khó chịu, vì vậy jitter nhận thức được rằng có một trình gỡ lỗi được đính kèm. Sau đó nó sửa đổibảng và thay đổi địa chỉ "sử dụng cuối cùng". Và thay đổi nó từ giá trị bình thường của nó thành địa chỉ của lệnh cuối cùng trong phương thức. Điều này giữ cho biến còn sống miễn là phương thức chưa được trả về. Cho phép bạn tiếp tục xem nó cho đến khi phương thức trở lại.

Điều này bây giờ cũng giải thích những gì bạn đã thấy trước đó và tại sao bạn đặt câu hỏi. Nó in "0" vì lệnh gọi GC.Collect không thể thu thập tham chiếu. Bảng này nói rằng biến đang được sử dụng trong lệnh gọi GC.Collect (), cho đến hết phương thức. Buộc phải nói như vậy bằng cách đính kèm trình gỡ lỗi chạy bản dựng Debug.

Đặt biến thành null sẽ có hiệu lực ngay bây giờ vì GC sẽ kiểm tra biến và sẽ không còn thấy tham chiếu. Nhưng hãy chắc chắn rằng bạn không rơi vào cái bẫy mà nhiều lập trình viên C # đã rơi vào, thực sự viết mã đó là vô nghĩa. Không có sự khác biệt nào cho dù câu lệnh đó có hay không khi bạn chạy mã trong bản dựng Phát hành. Trong thực tế, trình tối ưu hóa jitter sẽ loại bỏ câu lệnh đó vì nó không có tác dụng gì. Vì vậy, hãy chắc chắn không viết mã như vậy, mặc dù nó dường như có hiệu lực.


Một lưu ý cuối cùng về chủ đề này, đây là điều khiến các lập trình viên gặp rắc rối khi viết các chương trình nhỏ để làm một cái gì đó với ứng dụng Office. Trình gỡ lỗi thường đưa chúng vào Đường dẫn sai, chúng muốn chương trình Office thoát theo yêu cầu. Cách thích hợp để làm điều đó là bằng cách gọi GC.Collect (). Nhưng họ sẽ phát hiện ra rằng nó không hoạt động khi họ gỡ lỗi ứng dụng của họ, dẫn họ đến vùng đất không bao giờ không bao giờ bằng cách gọi Marshal.ReleaseComObject (). Quản lý bộ nhớ thủ công, nó hiếm khi hoạt động đúng bởi vì chúng sẽ dễ dàng bỏ qua một tham chiếu giao diện vô hình. GC.Collect () thực sự hoạt động, chỉ khi bạn gỡ lỗi ứng dụng.


1
Cũng xem câu hỏi của tôi mà Hans đã trả lời độc đáo cho tôi. stackoverflow.com/questions/15561025/ Mạnh
Dave Nay

1
@HansPassant Tôi vừa tìm thấy lời giải thích tuyệt vời này, cũng trả lời một phần câu hỏi của tôi ở đây: stackoverflow.com/questions/30529379/ trên về GC và đồng bộ hóa luồng. Một câu hỏi mà tôi vẫn có: Tôi tự hỏi liệu GC có thực sự nén và cập nhật các địa chỉ được sử dụng trong một thanh ghi (được lưu trong bộ nhớ trong khi bị treo) hay chỉ bỏ qua chúng? Một quá trình cập nhật các thanh ghi sau khi tạm dừng luồng (trước khi tiếp tục) đối với tôi giống như một luồng bảo mật nghiêm trọng bị HĐH chặn.
đúng

Một cách gián tiếp, vâng. Các luồng bị đình chỉ, GC cập nhật các cửa hàng sao lưu cho các thanh ghi CPU. Khi luồng tiếp tục chạy, bây giờ nó sử dụng các giá trị đăng ký được cập nhật.
Hans Passant

1
@HansPassant, tôi sẽ đánh giá cao nếu bạn thêm tài liệu tham khảo cho một số chi tiết không rõ ràng của trình thu gom rác CLR mà bạn mô tả ở đây?
denfromufa

Có vẻ như cấu hình khôn ngoan, một điểm quan trọng là "Tối ưu hóa mã" ( <Optimize>true</Optimize>trong .csproj) được bật. Đây là mặc định trong cấu hình "Phát hành". Nhưng trong trường hợp một người sử dụng các cấu hình tùy chỉnh, có liên quan để biết rằng cài đặt này là quan trọng.
Zero3

34

[Chỉ muốn thêm vào quá trình Hoàn thiện Nội bộ]

Vì vậy, bạn tạo một đối tượng và khi đối tượng được thu thập, Finalizephương thức của đối tượng sẽ được gọi. Nhưng có nhiều thứ để hoàn thiện hơn giả định rất đơn giản này.

KHÁI NIỆM NGẮN HẠN ::

  1. Đối tượng KHÔNG thực hiện Finalizecác phương thức, có Bộ nhớ được lấy lại ngay lập tức, trừ khi tất nhiên, chúng không thể truy cập được bằng
    mã ứng dụng nữa

  2. Objects thực hiện FinalizePhương pháp, The Concept / Thực hiện Application Roots, Finalization Queue, Freacheable Queueđến trước khi họ có thể được tái sinh.

  3. Bất kỳ đối tượng nào được coi là rác nếu Mã ứng dụng KHÔNG thể truy cập được

Giả :: Lớp học / Objects A, B, D, G, H không thực hiện FinalizePhương pháp và C, E, F, I, J thực hiện Finalizephương pháp.

Khi một ứng dụng tạo một đối tượng mới, toán tử mới phân bổ bộ nhớ từ heap. Nếu kiểu của đối tượng chứa một Finalizephương thức, thì một con trỏ tới đối tượng được đặt trên hàng đợi hoàn thiện .

do đó con trỏ tới các đối tượng C, E, F, I, J được thêm vào hàng đợi hoàn thiện.

Các hàng đợi quyết toán là một cấu trúc dữ liệu nội bộ điều khiển bởi các bộ thu rác. Mỗi mục trong hàng đợi trỏ đến một đối tượng nên có Finalizephương thức được gọi trước khi bộ nhớ của đối tượng có thể được lấy lại. Hình dưới đây cho thấy một đống chứa một số đối tượng. Một số đối tượng này có thể truy cập từ gốc của ứng dụngvà một số thì không. Khi các đối tượng C, E, F, I và J được tạo, khung .Net phát hiện ra rằng các đối tượng này có Finalizecác phương thức và con trỏ tới các đối tượng này được thêm vào hàng đợi hoàn thiện .

nhập mô tả hình ảnh ở đây

Khi xảy ra GC (Bộ sưu tập thứ 1), các đối tượng B, E, G, H, I và J được xác định là rác. Bởi vì A, C, D, F vẫn có thể truy cập bằng Mã ứng dụng được mô tả thông qua các mũi tên từ Hộp màu vàng ở trên.

Trình thu gom rác quét hàng đợi quyết toán tìm kiếm con trỏ tới các đối tượng này. Khi tìm thấy một con trỏ, con trỏ sẽ bị xóa khỏi hàng đợi hoàn thiện và được thêm vào hàng đợi có thể hiểu được ("F- Reachable ").

Các hàng đợi freachable là một cấu trúc dữ liệu nội bộ điều khiển bởi các bộ thu rác. Mỗi con trỏ trong hàng đợi có thể xác định được xác định một đối tượng đã sẵn sàng để Finalizegọi phương thức của nó .

Sau bộ sưu tập (Bộ sưu tập thứ 1), heap được quản lý trông giống như hình dưới đây. Giải thích được đưa ra dưới đây ::
1.) Bộ nhớ bị chiếm bởi các đối tượng B, G và H đã được lấy lại ngay lập tức vì các đối tượng này không có phương thức hoàn thiện cần được gọi .

2.) Tuy nhiên, không thể lấy lại bộ nhớ của các đối tượng E, I và J vì Finalizephương thức của chúng chưa được gọi. Gọi phương thức Finalize được thực hiện bằng hàng đợi có thể hiểu được.

3.) A, C, D, F vẫn có thể truy cập bằng Mã ứng dụng được mô tả qua các mũi tên từ Hộp màu vàng ở trên, vì vậy chúng sẽ KHÔNG được thu thập trong mọi trường hợp

nhập mô tả hình ảnh ở đây

Có một luồng thời gian chạy đặc biệt dành riêng để gọi các phương thức Finalize. Khi hàng đợi có thể xóa được (thường là trường hợp này), luồng này sẽ ngủ. Nhưng khi các mục xuất hiện, luồng này sẽ đánh thức, xóa từng mục khỏi hàng đợi và gọi phương thức Finalize của từng đối tượng. Các nhà sưu tập rác làm gọn bộ nhớ có thể khai phá và sợi runtime đặc biệt đổ các freachable hàng đợi, thực hiện của từng đối tượng Finalizephương pháp. Vì vậy, cuối cùng ở đây là khi phương thức Finalize của bạn được thực thi

Lần sau khi trình thu gom rác được gọi (Bộ sưu tập thứ 2), nó sẽ thấy rằng các đối tượng được hoàn thành là rác thực sự, vì các gốc của ứng dụng không trỏ đến nó và hàng đợi có thể xáo trộn không còn trỏ đến nó nữa (do đó cũng là EMPTY) bộ nhớ cho các đối tượng (E, I, J) được lấy lại đơn giản từ Heap. Xem hình bên dưới và so sánh nó với hình ở trên

nhập mô tả hình ảnh ở đây

Điều quan trọng cần hiểu ở đây là hai GC được yêu cầu để lấy lại bộ nhớ được sử dụng bởi các đối tượng yêu cầu hoàn thiện . Trong thực tế, nhiều hơn hai bộ sưu tập taxi thậm chí được yêu cầu vì các đối tượng này có thể được thăng cấp lên thế hệ cũ

LƯU Ý :: Các hàng đợi freachable được coi là một gốc giống như các biến toàn cục và tĩnh là rễ. Do đó, nếu một đối tượng nằm trong hàng đợi có thể hiểu được, thì đối tượng có thể truy cập và không phải là rác.

Lưu ý cuối cùng, hãy nhớ rằng ứng dụng gỡ lỗi là một thứ, Garbage Collection là một thứ khác và hoạt động khác đi. Cho đến nay, bạn không thể CẢM NHẬN bộ sưu tập rác chỉ bằng cách gỡ lỗi các ứng dụng, hơn nữa nếu bạn muốn điều tra Bộ nhớ bắt đầu ở đây.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.