Tại sao Bộ sưu tập rác chỉ quét đống?


28

Về cơ bản, tôi đã học được cho đến nay rằng bộ sưu tập rác sẽ xóa vĩnh viễn mọi cấu trúc dữ liệu hiện không được trỏ đến. Nhưng điều này chỉ kiểm tra đống cho các điều kiện như vậy.

Tại sao nó cũng không kiểm tra phần dữ liệu (toàn cầu, hằng số, v.v.) hoặc ngăn xếp? Điều gì về đống mà nó là thứ duy nhất mà chúng ta muốn là rác được thu thập?


21
"quét đống" an toàn hơn "quét đống" ... :-)
Brian Knoblauch

Câu trả lời:


62

Trình thu gom rác sẽ quét ngăn xếp - để xem những thứ trong đống hiện đang được sử dụng (chỉ vào) bởi những thứ trên ngăn xếp.

Thật vô nghĩa khi người thu gom rác xem xét việc thu thập bộ nhớ ngăn xếp vì ngăn xếp không được quản lý theo cách đó: Mọi thứ trên ngăn xếp được coi là "đang sử dụng". Và bộ nhớ được sử dụng bởi ngăn xếp sẽ tự động được lấy lại khi bạn quay lại từ các cuộc gọi phương thức. Quản lý bộ nhớ của không gian ngăn xếp rất đơn giản, rẻ tiền và dễ dàng đến mức bạn không muốn tham gia thu gom rác.

(Có các hệ thống, chẳng hạn như smalltalk, trong đó các khung ngăn xếp là các đối tượng hạng nhất được lưu trữ trong đống và rác được thu thập như tất cả các đối tượng khác. Nhưng đó không phải là cách tiếp cận phổ biến hiện nay. JVM của Java và CLR sử dụng ngăn xếp phần cứng và bộ nhớ liền kề .)


7
+1 ngăn xếp luôn có thể truy cập đầy đủ vì vậy không có ý nghĩa gì để quét nó
ratchet freak

2
+1 cảm ơn bạn, đã lấy 4 bài viết để trả lời đúng. Tôi không biết lý do tại sao bạn phải nói rằng tất cả mọi thứ trên ngăn xếp được "coi là" đang được sử dụng, nó được sử dụng ít nhất là ý nghĩa mạnh mẽ như các vật thể đống vẫn đang được sử dụng - nhưng đó là một nitlog thực sự của một câu trả lời rất tốt
psr

@psr anh ta có nghĩa là mọi thứ trên ngăn xếp đều có thể truy cập mạnh mẽ và không cần phải thu thập cho đến khi phương thức trở lại nhưng (RAII) đã được quản lý rõ ràng
ratchet freak

@ratchetfreak - Tôi biết. Và tôi chỉ có nghĩa là từ "được coi" có lẽ là không cần thiết, nó ổn để đưa ra tuyên bố mạnh mẽ hơn mà không cần nó.
psr

5
@psr: Tôi không đồng ý. " được coi là đang sử dụng" là chính xác hơn cho cả stack và heap, vì những lý do rất quan trọng. Những gì bạn muốn là loại bỏ những gì sẽ không được sử dụng lại; những gì bạn làm là bạn loại bỏ những gì không thể tiếp cận . Bạn cũng có thể có dữ liệu có thể truy cập mà bạn sẽ không bao giờ cần; khi dữ liệu này phát triển, bạn bị rò rỉ bộ nhớ (vâng, chúng có thể xảy ra ngay cả trong các ngôn ngữ của GC, không giống như nhiều người nghĩ). Và người ta có thể lập luận rằng rò rỉ ngăn xếp cũng xảy ra, ví dụ phổ biến nhất là các khung ngăn xếp không cần thiết trong các chương trình đệ quy đuôi chạy mà không loại bỏ cuộc gọi đuôi (ví dụ trên JVM).
Blaisorblade

19

Xoay câu hỏi của bạn xung quanh. Câu hỏi thúc đẩy thực sự là trong những trường hợp nào chúng ta có thể tránh được các chi phí thu gom rác?

Vâng, trước hết, những gì chi phí cho việc thu gom rác thải? Có hai chi phí chính. Đầu tiên, bạn phải xác định cái gì còn sống ; điều đó đòi hỏi tiềm năng rất nhiều công việc Thứ hai, bạn phải thu gọn các lỗ hổng được hình thành khi bạn giải phóng thứ gì đó được phân bổ giữa hai thứ vẫn còn sống. Những cái lỗ đó thật lãng phí. Nhưng nén chúng cũng đắt tiền.

Làm thế nào chúng ta có thể tránh những chi phí này?

Rõ ràng nếu bạn có thể tìm thấy một mô hình sử dụng lưu trữ mà bạn không bao giờ phân bổ một cái gì đó tồn tại lâu dài, sau đó phân bổ một cái gì đó tồn tại trong thời gian ngắn, sau đó phân bổ một cái gì đó tồn tại lâu dài, bạn có thể loại bỏ chi phí lỗ hổng. Nếu bạn có thể đảm bảo rằng đối với một số tập hợp con của bộ lưu trữ của bạn, mọi phân bổ tiếp theo sẽ có thời gian sử dụng ngắn hơn bộ lưu trữ trước đó, thì sẽ không bao giờ có bất kỳ lỗ hổng nào trong bộ lưu trữ đó.

Nhưng nếu chúng ta giải quyết được vấn đề lỗ thì chúng ta cũng đã giải quyết vấn đề thu gom rác . Bạn có một cái gì đó trong kho lưu trữ vẫn còn sống? Vâng. Là tất cả mọi thứ được phân bổ trước khi nó sống lâu hơn? Vâng - giả định đó là cách chúng tôi loại bỏ khả năng lỗ hổng. Do đó, tất cả những gì bạn cần làm là nói "phân bổ gần đây nhất còn sống?" và bạn biết rằng mọi thứ đều sống trong kho lưu trữ đó.

Chúng ta có một bộ phân bổ lưu trữ mà chúng ta biết rằng mọi phân bổ tiếp theo có thời gian tồn tại ngắn hơn phân bổ trước đó không? Vâng! Các khung kích hoạt của các phương thức luôn bị phá hủy theo thứ tự ngược lại mà chúng được tạo bởi vì chúng luôn có thời gian tồn tại ngắn hơn kích hoạt đã tạo ra chúng.

Do đó, chúng ta có thể lưu trữ các khung kích hoạt trên ngăn xếp và biết rằng chúng không bao giờ cần phải được thu thập. Nếu có bất kỳ khung nào trên ngăn xếp, toàn bộ bộ khung bên dưới nó sẽ tồn tại lâu hơn, vì vậy chúng không cần phải được thu thập. Và chúng sẽ bị phá hủy theo thứ tự ngược lại mà chúng được tạo ra. Do đó, chi phí cho việc thu gom rác được loại bỏ cho các khung kích hoạt.

Đó là lý do tại sao chúng ta có nhóm tạm thời trên ngăn xếp ở vị trí đầu tiên: bởi vì đây là cách dễ dàng để thực hiện kích hoạt phương thức mà không bị phạt quản lý bộ nhớ.

(Tất nhiên là chi phí thu gom rác bộ nhớ được tham chiếu bởi các tham chiếu trên các khung kích hoạt vẫn còn đó.)

Bây giờ hãy xem xét một hệ thống luồng điều khiển trong đó các khung kích hoạt không bị phá hủy theo thứ tự dự đoán. Điều gì xảy ra nếu kích hoạt trong thời gian ngắn có thể làm phát sinh kích hoạt lâu dài? Như bạn có thể tưởng tượng, trong thế giới này, bạn không còn có thể sử dụng ngăn xếp để tối ưu hóa nhu cầu thu thập kích hoạt. Tập hợp kích hoạt có thể chứa lỗ một lần nữa.

C # 2.0 có tính năng này ở dạng yield return. Một phương thức thực hiện trả lại lợi tức sẽ được kích hoạt lại vào lần sau - lần tiếp theo mà MoveNext được gọi - và khi điều đó xảy ra là không thể dự đoán được. Do đó, thông tin thường có trên ngăn xếp cho khung kích hoạt của khối lặp thay vào đó được lưu trữ trên heap, nơi nó được thu gom rác khi thu thập dữ liệu.

Tương tự, tính năng "async / await" có trong các phiên bản tiếp theo của C # và VB sẽ cho phép bạn tạo các phương thức có kích hoạt "nhường" và "tiếp tục" tại các điểm được xác định rõ trong quá trình hoạt động của phương thức. Vì các khung kích hoạt không còn được tạo và hủy theo cách có thể dự đoán được, tất cả thông tin đã từng được lưu trữ trong ngăn xếp sẽ phải được lưu trữ trong heap.

Đó chỉ là một tai nạn của lịch sử mà chúng tôi đã quyết định trong một vài thập kỷ rằng các ngôn ngữ có khung kích hoạt được tạo ra và phá hủy theo một trật tự nghiêm ngặt là thời thượng. Vì các ngôn ngữ hiện đại ngày càng thiếu tài sản này, hy vọng sẽ thấy ngày càng nhiều ngôn ngữ thống nhất các phần tiếp theo vào đống rác được thu gom, thay vì ngăn xếp.


13

Câu trả lời rõ ràng nhất và có lẽ không đầy đủ nhất là heap là vị trí của dữ liệu cá thể. Theo dữ liệu cá thể, chúng tôi có nghĩa là dữ liệu đại diện cho các thể hiện của các lớp, còn gọi là các đối tượng, được tạo ra trong thời gian chạy. Dữ liệu này vốn đã động và số lượng các đối tượng này, và do đó, lượng bộ nhớ chúng chiếm, chỉ được biết khi chạy. Đã có một số vấn đề về phục hồi bộ nhớ này hoặc các chương trình chạy dài sẽ tiêu tốn tất cả bộ nhớ theo thời gian.

Bộ nhớ được sử dụng bởi các khiếm khuyết lớp, hằng số và các cấu trúc dữ liệu tĩnh khác vốn không có khả năng tăng không được kiểm soát. Vì chỉ có một định nghĩa lớp duy nhất trong bộ nhớ cho một số lượng thời gian chạy không xác định của lớp đó, nên có nghĩa là loại cấu trúc này không phải là mối đe dọa đối với việc sử dụng bộ nhớ.


5
Nhưng heap không phải là vị trí của dữ liệu dụ dụ. Họ có thể ở trên ngăn xếp quá.
Svick

@svick Phụ thuộc vào ngôn ngữ, tất nhiên. Java chỉ hỗ trợ các đối tượng được phân bổ heap và Vala phân biệt khá rõ ràng giữa phân bổ heap (lớp) và phân bổ stack (struct).
fluffy

1
@fluffy: đó là những ngôn ngữ rất hạn chế, bạn không thể cho rằng ngôn ngữ này nói chung vì không có ngôn ngữ nào được đặt trước.
Matthieu M.

@MatthieuM. Đó là loại quan điểm của tôi.
fluffy

@fluffy: vậy tại sao các lớp được phân bổ trong heap, trong khi các cấu trúc được phân bổ trong ngăn xếp?
Dark Templar

10

Điều đáng ghi nhớ là lý do tại sao chúng ta có bộ sưu tập rác: bởi vì đôi khi thật khó để biết khi nào nên giải phóng bộ nhớ. Bạn thực sự chỉ có vấn đề này với đống. Dữ liệu được phân bổ trên ngăn xếp cuối cùng sẽ được xử lý, do đó thực sự không cần phải thu gom rác ở đó. Những thứ trong phần dữ liệu thường được giả định là được phân bổ cho thời gian tồn tại của chương trình.


1
Nó không chỉ được giải quyết 'cuối cùng' mà nó sẽ được giải quyết vào đúng thời điểm.
Boris Yankov

3
  1. Kích thước của chúng có thể dự đoán được (không đổi ngoại trừ ngăn xếp và ngăn xếp thường được giới hạn ở một vài MB) và thường rất nhỏ (ít nhất là so với hàng trăm ứng dụng lớn có thể phân bổ).

  2. Các đối tượng được phân bổ động thường có khung thời gian nhỏ trong đó chúng có thể truy cập được. Sau đó, không có cách nào họ có thể được tham khảo một lần nữa. Ngược lại với các mục trong phần dữ liệu, các biến toàn cục và như vậy: Thường xuyên, có một đoạn mã tham chiếu trực tiếp đến chúng (nghĩ const char *foo() { return "foo"; }). Thông thường, mã không thay đổi, vì vậy tham chiếu vẫn ở đó và một tham chiếu khác sẽ được tạo mỗi khi hàm được gọi (có thể là bất cứ lúc nào theo như máy tính biết - trừ khi bạn giải quyết vấn đề tạm dừng, đó là ). Do đó, bạn không thể giải phóng hầu hết bộ nhớ đó, vì nó sẽ luôn có thể truy cập được.

  3. Trong nhiều ngôn ngữ được thu gom rác, mọi thứ thuộc về chương trình đang chạy đều được phân bổ theo đống. Trong Python, đơn giản là không có bất kỳ phần dữ liệu nào và không có giá trị được phân bổ ngăn xếp (có các tham chiếu có các biến cục bộ và có ngăn xếp cuộc gọi, nhưng không phải là một giá trị theo nghĩa tương tự như inttrong C). Mọi đối tượng là trên đống.


"Trong Python, đơn giản là không có phần dữ liệu nào". Điều này không đúng. Không, Đúng và Sai được phân bổ trong phần dữ liệu theo tôi hiểu: stackoverflow.com/questions/7681786/how-is-hashnone-calculated
Jason Baker

@JasonBaker: Tìm thấy thú vị! Nó không có tác dụng gì cả. Đó là một chi tiết triển khai và giới hạn đối với các đối tượng dựng sẵn. Đó là chưa kể rằng những đối tượng không được dự kiến sẽ được deallocated bao giờ trong cuộc đời của chương trình dù sao, không phải là, và cũng có thể là rất nhỏ trong kích thước (nhỏ hơn 32 byte mỗi, tôi đoán).

@delnan Như Eric Lippert thích chỉ ra, đối với hầu hết các ngôn ngữ, sự tồn tại của các vùng bộ nhớ riêng biệt cho ngăn xếp và đống là một chi tiết triển khai. Bạn có thể thực hiện hầu hết các ngôn ngữ mà không cần sử dụng ngăn xếp (mặc dù hiệu suất có thể bị ảnh hưởng khi bạn làm) và vẫn tuân thủ thông số kỹ thuật của chúng
Jules

2

Như một số người trả lời khác đã nói, ngăn xếp là một phần của tập hợp gốc, vì vậy nó được quét để tham khảo nhưng không được "thu thập", mỗi lần.

Tôi chỉ muốn trả lời một số ý kiến ​​ngụ ý rằng rác trên ngăn xếp không thành vấn đề; bởi vì nó có thể gây ra nhiều rác hơn trong đống để được coi là có thể truy cập được. Người viết trình biên dịch và trình biên dịch có lương tâm hoặc loại trừ hoặc loại trừ các phần chết của ngăn xếp khỏi quá trình quét. IIRC, một số máy ảo có bảng ánh xạ phạm vi PC đến bitmap khe-khe-khe và các loại khác chỉ loại bỏ các khe. Tôi không biết kỹ thuật nào hiện đang được ưa thích.

Một thuật ngữ được sử dụng để mô tả sự cân nhắc đặc biệt này là an toàn cho không gian .


Sẽ rất thú vị khi biết. Suy nghĩ đầu tiên là vô hiệu hóa không gian là thực tế nhất. Đi qua một cây gồm các khu vực bị loại trừ có thể mất nhiều thời gian hơn là chỉ quét qua null. Rõ ràng là bất kỳ nỗ lực nào để thu gọn stack đều đầy nguy hiểm! Làm cho công việc đó nghe có vẻ như là một quá trình thiên về suy nghĩ / lỗi.
Brian Knoblauch

@Brian, Trên thực tế, nghĩ thêm về nó, đối với một máy ảo được nhập, bạn vẫn cần một cái gì đó tương tự, vì vậy bạn có thể xác định vị trí nào là tham chiếu trái ngược với số nguyên, số float, v.v. Ngoài ra, về việc nén ngăn xếp, hãy xem Không tham khảo lý lẽ của nó "của Henry Baker.
Ryan Culpepper

Xác định các loại vị trí và xác minh rằng chúng được sử dụng một cách thích hợp có thể và thường được thực hiện tĩnh, tại thời gian biên dịch (đối với máy ảo sử dụng mã byte đáng tin cậy) hoặc thời gian tải (trong đó mã byte đến từ nguồn không tin cậy, ví dụ Java).
Jules

1

Hãy để tôi chỉ ra một vài hiểu lầm cơ bản mà bạn và nhiều người khác đã sai:

"Tại sao Bộ sưu tập rác chỉ quét đống?" Đó là cách khác. Chỉ những người thu gom rác đơn giản nhất, bảo thủ nhất và chậm nhất mới quét được đống. Đó là lý do tại sao họ rất chậm.

Trình thu gom rác nhanh chỉ quét ngăn xếp (và tùy chọn một số gốc khác, như một số toàn cầu cho con trỏ FFI và các thanh ghi cho con trỏ trực tiếp) và chỉ sao chép các con trỏ có thể tiếp cận được bởi các đối tượng ngăn xếp. Phần còn lại bị vứt đi (tức là bị bỏ qua), không quét toàn bộ đống.

Do heap lớn hơn khoảng 1000 lần so với (các) ngăn xếp, nên một GC quét ngăn xếp như vậy thường nhanh hơn nhiều. ~ 15ms so với 250ms trên đống kích thước bình thường. Vì nó sao chép (di chuyển) các đối tượng từ không gian này sang không gian khác, nên nó chủ yếu được gọi là bộ thu sao chép bán không gian, nó cần bộ nhớ gấp 2 lần và do đó hầu như không sử dụng được trên các thiết bị rất nhỏ như điện thoại không có nhiều bộ nhớ. Nó được nén, do đó, nó rất thân thiện với bộ nhớ cache, không giống như các máy quét heap mark & ​​scan đơn giản.

Vì đó là con trỏ di chuyển, FFI, danh tính và tài liệu tham khảo rất khó. Danh tính thường được giải quyết với id ngẫu nhiên, tham chiếu thông qua con trỏ chuyển tiếp. FFI là khó khăn, vì các đối tượng nước ngoài không thể giữ con trỏ trở lại không gian cũ. Con trỏ FFI thường được giữ trong một vùng heap riêng, ví dụ với dấu chậm & quét, bộ thu tĩnh. Hoặc malloc tầm thường với đếm. Lưu ý rằng malloc có một chi phí rất lớn và thậm chí còn nhiều hơn thế.

Mark & ​​quét là không quan trọng để thực hiện nhưng nó không nên được sử dụng trong các chương trình thực tế và đặc biệt không được dạy như là người thu thập tiêu chuẩn. Nổi tiếng nhất của trình thu thập sao chép quét nhanh như vậy được gọi là trình thu thập hai ngón tay Cheney .


Câu hỏi dường như liên quan nhiều hơn đến phần nào của bộ nhớ là rác được thu thập, thay vì các thuật toán thu gom rác cụ thể. Câu cuối cùng đặc biệt ngụ ý OP đang sử dụng "quét" làm từ đồng nghĩa chung cho "thu gom rác", chứ không phải là một cơ chế cụ thể để thực hiện thu gom rác. Xem xét điều đó, câu trả lời của bạn xuất hiện khi nói rằng chỉ những người thu gom rác đơn giản nhất mới thu gom được đống, và những người thu gom rác nhanh thay vì rác thu gom đống và bộ nhớ tĩnh, để lại đống lớn lên và phát triển cho đến khi hết bộ nhớ.
8bittree

Không, câu hỏi rất cụ thể và thông minh. Các câu trả lời không phải vậy. Dấu chậm & quét của GC có hai giai đoạn, bước đánh dấu quét gốc trên ngăn xếp và giai đoạn quét quét đống. Sao chép nhanh chóng chỉ có một pha, quét ngăn xếp. Dễ như thế. Vì rõ ràng không ai biết ở đây về người thu gom rác thích hợp, câu hỏi cần được trả lời. Giải thích của bạn là hoang dã tắt.
rurban

0

Những gì được phân bổ trên ngăn xếp? Biến cục bộ và địa chỉ trả về (bằng C). Khi một hàm trả về, các biến cục bộ của nó bị loại bỏ. Nó không phải là không cần thiết, thậm chí là bất lợi, để quét ngăn xếp.

Nhiều ngôn ngữ động và cả Java hoặc C # được triển khai bằng ngôn ngữ lập trình hệ thống, thường là C. Bạn có thể nói Java được triển khai với các hàm C và sử dụng các biến cục bộ C và do đó, trình thu gom rác của Java không cần quét ngăn xếp.

Có một ngoại lệ thú vị: Trình thu gom rác của Chicken Scheme thực hiện quét ngăn xếp (theo cách nào đó), vì cách triển khai của nó sử dụng ngăn xếp như một không gian thế hệ thứ nhất của bộ sưu tập rác: xem Wikipedia Thiết kế sơ đồ gà .

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.