Tại sao các đối tượng Java không bị xóa ngay lập tức sau khi chúng không còn được tham chiếu?


77

Trong Java, ngay khi một đối tượng không còn bất kỳ tham chiếu nào nữa, nó sẽ trở thành đủ điều kiện để xóa, nhưng JVM quyết định khi nào đối tượng thực sự bị xóa. Để sử dụng thuật ngữ Objective-C, tất cả các tham chiếu Java vốn đã "mạnh". Tuy nhiên, trong Objective-C, nếu một đối tượng không còn tham chiếu mạnh nữa, đối tượng sẽ bị xóa ngay lập tức. Tại sao đây không phải là trường hợp trong Java?


46
Bạn không nên quan tâm khi các đối tượng Java thực sự bị xóa. Đây là một chi tiết thực hiện.
Basile Starynkevitch

154
@BasileStarynkevitch Bạn hoàn toàn nên quan tâm và thách thức cách hệ thống / nền tảng của bạn hoạt động. Đặt câu hỏi 'làm thế nào' và 'tại sao' là một trong những cách tốt nhất để trở thành lập trình viên tốt hơn (và, theo nghĩa chung hơn, người thông minh hơn).
Artur Biesiadowski

6
Mục tiêu C làm gì khi có các tham chiếu tròn? Tôi cho rằng nó chỉ rò rỉ chúng?
Mehrdad

45
@ArturBiesiadowksi: Không, đặc tả Java không cho biết khi nào một đối tượng bị xóa (và tương tự, đối với R5RS ). Bạn có thể và có lẽ nên phát triển chương trình Java của mình - nếu việc xóa đó không bao giờ xảy ra (và đối với các quy trình tồn tại ngắn như thế giới xin chào Java, thì điều đó thực sự không xảy ra). Bạn có thể quan tâm đến tập hợp các đối tượng sống (hoặc mức tiêu thụ bộ nhớ), đó là một câu chuyện khác nhau.
Basile Starynkevitch

28
Một ngày nọ, người mới nói với chủ "Tôi có một giải pháp cho vấn đề phân bổ của chúng tôi. Chúng tôi sẽ cung cấp cho mỗi phân bổ một số tham chiếu và khi nó đạt đến 0, chúng tôi có thể xóa đối tượng". Ông chủ trả lời "Một ngày nọ, người mới nói với thầy" Tôi có một giải pháp ...
Eric Lippert

Câu trả lời:


79

Trước hết, Java có các tài liệu tham khảo yếu và một loại nỗ lực tốt nhất khác gọi là tài liệu tham khảo mềm. Tham chiếu yếu so với tham chiếu mạnh là một vấn đề hoàn toàn tách biệt với đếm tham chiếu so với thu gom rác.

Thứ hai, có những kiểu sử dụng bộ nhớ có thể giúp việc thu gom rác hiệu quả hơn theo thời gian bằng cách hy sinh không gian. Ví dụ, các đối tượng mới hơn có nhiều khả năng bị xóa hơn các đối tượng cũ hơn. Vì vậy, nếu bạn chờ một chút giữa các lần quét, bạn có thể xóa hầu hết thế hệ bộ nhớ mới, đồng thời chuyển một vài người sống sót sang lưu trữ lâu dài. Lưu trữ dài hạn có thể được quét ít thường xuyên hơn. Xóa ngay lập tức thông qua quản lý bộ nhớ thủ công hoặc đếm tham chiếu dễ bị phân mảnh hơn nhiều.

Nó giống như sự khác biệt giữa đi mua hàng tạp hóa một lần cho mỗi lần trả lương và mỗi ngày để có đủ thực phẩm cho một ngày. Một chuyến đi lớn của bạn sẽ mất nhiều thời gian hơn một chuyến đi nhỏ, nhưng nhìn chung bạn sẽ tiết kiệm được thời gian và tiền bạc.


58
Vợ của một lập trình viên gửi anh ta đến siêu thị. Cô nói với anh ta, "Mua một ổ bánh mì, và nếu bạn thấy vài quả trứng, hãy lấy một tá." Lập trình viên sau đó trở lại với hàng tá ổ bánh mì dưới tay.
Neil

7
Tôi đề nghị đề cập rằng thời gian gc thế hệ mới thường tỷ lệ thuận với số lượng đối tượng sống , do đó, có nhiều đối tượng bị xóa hơn có nghĩa là chi phí của họ sẽ không được trả trong nhiều trường hợp. Xóa đơn giản như lật con trỏ không gian sống sót và tùy ý xóa toàn bộ không gian bộ nhớ trong một bộ nhớ lớn (không chắc là nó được thực hiện ở cuối gc hoặc được khấu hao trong khi phân bổ tlabs hoặc các đối tượng trong jvms hiện tại)
Artur Biesiadowski

64
@ Không nên là 13 cái bánh?
JAD

67
"Tắt bởi một lỗi trên lối đi 7"
joeytwiddle

13
@JAD Tôi đã nói 13, nhưng hầu hết không có xu hướng để có được điều đó. ;)
Neil

86

Bởi vì biết chính xác một cái gì đó không còn được tham chiếu là không dễ dàng. Thậm chí không gần dễ dàng.

Nếu bạn có hai đối tượng tham chiếu lẫn nhau thì sao? Họ có ở lại mãi mãi không? Mở rộng dòng suy nghĩ đó để giải quyết bất kỳ cấu trúc dữ liệu tùy ý nào và bạn sẽ sớm thấy lý do tại sao JVM hoặc các công cụ thu gom rác khác buộc phải sử dụng các phương pháp tinh vi hơn để xác định những gì vẫn cần và những gì có thể đi.


7
Hoặc bạn có thể thực hiện một cách tiếp cận Python trong đó bạn sử dụng tính năng đếm ngược càng nhiều càng tốt, dùng đến một GC khi bạn mong đợi có các phụ thuộc vòng tròn bị rò rỉ bộ nhớ. Tôi không thấy lý do tại sao họ không thể đếm được ngoài GC?
Mehrdad

27
@Mehrdad Họ có thể. Nhưng có lẽ nó sẽ chậm hơn. Không có gì ngăn cản bạn thực hiện điều này, nhưng đừng hy vọng đánh bại bất kỳ GC nào trong Hotspot hoặc OpenJ9.
Josef

21
@ jpmc26 vì nếu bạn xóa các đối tượng ngay khi chúng không được sử dụng nữa, xác suất cao là bạn xóa chúng trong tình huống tải cao làm tăng tải hơn nữa. GC có thể chạy khi có tải ít hơn. Tham chiếu đếm chính nó là một chi phí nhỏ cho mỗi tài liệu tham khảo. Ngoài ra với một GC, bạn thường có thể loại bỏ một phần lớn bộ nhớ mà không có tham chiếu mà không xử lý các đối tượng đơn lẻ.
Josef

33
@Josef: đếm tham chiếu thích hợp cũng không miễn phí; cập nhật số tham chiếu đòi hỏi tăng / giảm nguyên tử, chi phí đáng ngạc nhiên , đặc biệt là trên các kiến ​​trúc đa lõi hiện đại. Trong CPython, đó không phải là vấn đề lớn (CPython cực kỳ chậm và GIL giới hạn hiệu suất đa luồng của nó ở mức đơn lõi), nhưng với ngôn ngữ nhanh hơn cũng hỗ trợ song song thì đó có thể là một vấn đề. Đây không phải là cơ hội để PyPy loại bỏ hoàn toàn việc tham chiếu và chỉ sử dụng GC.
Matteo Italia

10
@Mehrdad một khi bạn đã triển khai tính năng tham chiếu GC cho Java, tôi sẽ sẵn sàng kiểm tra nó để tìm trường hợp nó thực hiện kém hơn bất kỳ triển khai GC nào khác.
Josef

45

AFAIK, đặc tả JVM (viết bằng tiếng Anh) không đề cập đến khi chính xác một đối tượng (hoặc một giá trị) nên bị xóa và để lại cho việc thực hiện (tương tự như vậy đối với R5RS ). Nó bằng cách nào đó yêu cầu hoặc đề xuất một trình thu gom rác nhưng để lại các chi tiết để thực hiện. Và tương tự như vậy đối với đặc tả Java.

Hãy nhớ rằng ngôn ngữ lập trìnhthông số kỹ thuật (về cú pháp , ngữ nghĩa , v.v ...), không phải triển khai phần mềm. Một ngôn ngữ như Java (hoặc JVM của nó) có nhiều triển khai. Đặc điểm kỹ thuật của nó được xuất bản , có thể tải xuống (vì vậy bạn có thể nghiên cứu nó) và viết bằng tiếng Anh. §2,5.3 Heap của đặc tả JVM đề cập đến trình thu gom rác:

Lưu trữ heap cho các đối tượng được thu hồi bởi một hệ thống quản lý lưu trữ tự động (được gọi là bộ thu gom rác); các đối tượng không bao giờ được giải quyết rõ ràng. Máy ảo Java giả định không có loại hệ thống quản lý lưu trữ tự động cụ thể nào

(nhấn mạnh là của tôi; quyết toán BTW được đề cập trong §12.6 của thông số Java và một mô hình bộ nhớ nằm trong §17.4 của thông số Java)

Vì vậy (trong Java) bạn không nên quan tâm khi một đối tượng bị xóa và bạn có thể viết mã như - nếu điều đó không xảy ra (bằng cách suy luận một cách trừu tượng trong đó bạn bỏ qua điều đó). Tất nhiên bạn cần quan tâm đến việc tiêu thụ bộ nhớ và tập hợp các đối tượng sống, đó là một câu hỏi khác nhau . Trong một số trường hợp đơn giản (nghĩ về chương trình "xin chào thế giới"), bạn có thể chứng minh - hoặc tự thuyết phục bản thân - rằng bộ nhớ được phân bổ khá nhỏ (ví dụ: ít hơn một gigabyte), và sau đó bạn không quan tâm xóa các đối tượng riêng lẻ . Trong nhiều trường hợp hơn, bạn có thể thuyết phục bản thân rằng các vật thể sống(hoặc những người có thể tiếp cận, là một siêu thay đổi - lý do hơn là về những người sống) không bao giờ vượt quá giới hạn hợp lý (và sau đó bạn dựa vào GC, nhưng bạn không quan tâm đến việc thu gom rác diễn ra như thế nào và khi nào). Đọc về sự phức tạp không gian .

Tôi đoán rằng trên một số triển khai JVM chạy chương trình Java có thời gian ngắn như thế giới xin chào, trình thu gom rác hoàn toàn không được kích hoạt và không xảy ra xóa. AFAIU, một hành vi như vậy phù hợp với nhiều thông số kỹ thuật Java.

Hầu hết các triển khai JVM sử dụng các kỹ thuật sao chép thế hệ (ít nhất là đối với hầu hết các đối tượng Java, những đối tượng không sử dụng hoàn thiện hoặc tham chiếu yếu ; và hoàn thiện không được đảm bảo xảy ra trong một thời gian ngắn và có thể bị hoãn lại, vì vậy đây chỉ là một tính năng hữu ích mà mã của bạn không nên phụ thuộc nhiều vào) trong đó khái niệm xóa một đối tượng riêng lẻ không có ý nghĩa gì (vì một khối lớn các vùng nhớ chứa bộ nhớ cho nhiều đối tượng-, có thể vài megabyte cùng một lúc, được giải phóng cùng một lúc).

Nếu đặc tả JVM yêu cầu mỗi đối tượng phải được xóa chính xác càng sớm càng tốt (hoặc đơn giản là đặt nhiều ràng buộc hơn vào việc xóa đối tượng), các kỹ thuật GC thế hệ hiệu quả sẽ bị cấm và các nhà thiết kế Java và JVM đã khôn ngoan tránh điều đó.

BTW, có thể là một JVM ngây thơ không bao giờ xóa các đối tượng và không giải phóng bộ nhớ có thể phù hợp với thông số kỹ thuật (chữ cái, không phải tinh thần) và chắc chắn có thể chạy một điều thế giới xin chào trong thực tế (chú ý rằng hầu hết các chương trình Java nhỏ và ngắn có lẽ không phân bổ nhiều hơn một vài gigabyte bộ nhớ). Tất nhiên một JVM như vậy không đáng để đề cập và chỉ là một thứ đồ chơi (giống như việc triển khai nàymalloc cho C). Xem Epsilon NoOp GC để biết thêm. Các JVM ngoài đời thực là những phần mềm rất phức tạp và trộn lẫn một số kỹ thuật thu gom rác.

Ngoài ra, Java không giống như JVM và bạn có các triển khai Java đang chạy mà không có JVM (ví dụ: các trình biên dịch Java trước thời hạn , thời gian chạy Android ). Trong một số trường hợp (chủ yếu là học thuật), bạn có thể tưởng tượng (được gọi là kỹ thuật "thu gom rác thời gian biên dịch") rằng chương trình Java không phân bổ hoặc xóa trong thời gian chạy (ví dụ: vì trình biên dịch tối ưu hóa đủ thông minh để chỉ sử dụng gọi ngăn xếpbiến tự động ).

Tại sao các đối tượng Java không bị xóa ngay lập tức sau khi chúng không còn được tham chiếu?

Bởi vì các đặc tả Java và JVM không yêu cầu điều đó.


Đọc cẩm nang GC để biết thêm (và thông số JVM ). Lưu ý rằng việc tồn tại (hoặc hữu ích cho tính toán trong tương lai) cho một đối tượng là thuộc tính toàn chương trình (không phải mô-đun).

Objective-C ủng hộ cách tiếp cận đếm tham chiếu đến quản lý bộ nhớ . Và điều đó cũng có những cạm bẫy (ví dụ, lập trình viên Objective-C phải quan tâm đến các tham chiếu vòng tròn bằng cách khám phá các tham chiếu yếu, nhưng một JVM xử lý các tham chiếu vòng tròn độc đáo trong thực tế mà không cần sự chú ý từ lập trình viên Java).

Không có Silver Bullet trong lập trình và thiết kế ngôn ngữ lập trình (lưu ý vấn đề Ngừng ; nói chung là một đối tượng sống hữu ích là không thể giải quyết được ).

Bạn cũng có thể đọc SICP , Lập trình ngôn ngữ lập trình , Sách rồng , Lisp trong các mảnh nhỏhệ điều hành: Ba mảnh dễ dàng . Chúng không phải là về Java, nhưng chúng sẽ mở mang đầu óc của bạn và sẽ giúp hiểu được JVM nên làm gì và nó có thể hoạt động như thế nào (với các phần khác) trên máy tính của bạn. Bạn cũng có thể dành nhiều tháng (hoặc vài năm) để nghiên cứu mã nguồn phức tạp của các triển khai JVM nguồn mở hiện có (như OpenJDK , có vài triệu dòng mã nguồn).


20
"có thể là một JVM ngây thơ không bao giờ xóa các đối tượng và không giải phóng bộ nhớ có thể phù hợp với thông số kỹ thuật" Nó chắc chắn phù hợp với thông số kỹ thuật! Java 11 thực sự đang thêm một trình thu gom rác không hoạt động, trong số những thứ khác, các chương trình rất ngắn.
Michael

6
"Bạn không nên quan tâm khi một đối tượng bị xóa" Không đồng ý. Đối với một người, bạn nên biết rằng RAII không còn là một mẫu khả thi nữa và bạn không thể phụ thuộc vào finalizebất kỳ quản lý tài nguyên nào (của tệp thủ công, kết nối db, tài nguyên gpu, v.v.).
Alexander

4
@Michael Thật hoàn hảo khi xử lý hàng loạt với trần bộ nhớ đã sử dụng. HĐH chỉ có thể nói "tất cả bộ nhớ được sử dụng bởi chương trình này đã biến mất!" Rốt cuộc, đó là khá nhanh. Thật vậy, nhiều chương trình trong C đã được viết theo cách đó, đặc biệt là trong thế giới Unix đầu tiên. Pascal có "con trỏ stack / heap" khủng khiếp đến điểm kiểm tra được lưu trước "cho phép bạn thực hiện nhiều việc tương tự, mặc dù nó khá không an toàn - đánh dấu, bắt đầu tác vụ phụ, đặt lại.
Luaan

6
@Alexander nói chung bên ngoài C ++ (và một vài ngôn ngữ cố ý xuất phát từ nó), giả sử RAII sẽ hoạt động chỉ dựa trên bộ hoàn thiện là một mô hình chống, một ngôn ngữ nên được cảnh báo chống lại và thay thế bằng khối kiểm soát tài nguyên rõ ràng. Rốt cuộc, toàn bộ quan điểm của GC là tuổi thọ và tài nguyên được tách rời.
Leushenko

3
@Leushenko Tôi hoàn toàn không đồng ý rằng "tuổi thọ và tài nguyên bị tách rời" là "toàn bộ điểm" của GC. Đó là mức giá tiêu cực mà bạn phải trả cho điểm chính của GC: quản lý bộ nhớ an toàn, dễ dàng. "Giả sử RAII sẽ hoạt động chỉ dựa trên bộ hoàn thiện là một mô hình chống" Trong Java? Có lẽ. Nhưng không phải trong CPython, Rust, Swift hay Objective C. "đã cảnh báo chống lại và thay thế bằng một khối kiểm soát tài nguyên rõ ràng" Không, những điều này bị hạn chế nghiêm ngặt hơn. Một đối tượng quản lý tài nguyên thông qua RAII cung cấp cho bạn một tay cầm để vượt qua vòng đời xung quanh. Một khối thử với tài nguyên được giới hạn trong một phạm vi duy nhất.
Alexander

23

Để sử dụng thuật ngữ Objective-C, tất cả các tham chiếu Java vốn đã "mạnh".

Điều đó không chính xác - Java có cả các tham chiếu yếu và mềm, mặc dù các tham chiếu này được triển khai ở cấp đối tượng thay vì từ khóa ngôn ngữ.

Trong Objective-C, nếu một đối tượng không còn tham chiếu mạnh nữa, đối tượng sẽ bị xóa ngay lập tức.

Điều đó cũng không hẳn đúng - một số phiên bản của Objective C thực sự đã sử dụng trình thu gom rác thế hệ. Các phiên bản khác không có bộ sưu tập rác nào cả.

Đúng là các phiên bản mới hơn của Objective C sử dụng tính năng tham chiếu tự động (ARC) thay vì GC dựa trên dấu vết và điều này (thường) dẫn đến việc đối tượng bị "xóa" khi số tham chiếu đó bằng không. Tuy nhiên, lưu ý rằng việc triển khai JVM cũng có thể được tuân thủ và hoạt động chính xác theo cách này (quái gì, nó có thể tuân thủ và không có GC nào cả.)

Vậy tại sao hầu hết các triển khai JVM không làm điều này và thay vào đó sử dụng các thuật toán GC dựa trên dấu vết?

Nói một cách đơn giản, ARC không phải là không tưởng như lúc đầu:

  • Bạn phải tăng hoặc giảm số lượt truy cập mỗi khi tham chiếu được sao chép, sửa đổi hoặc đi ra khỏi phạm vi, điều này mang lại một chi phí hiệu năng rõ ràng.
  • ARC không thể dễ dàng xóa các tham chiếu theo chu kỳ, vì tất cả chúng đều có tham chiếu với nhau, do đó, số tham chiếu của chúng không bao giờ bằng không.

Tất nhiên ARC có lợi thế - việc thực hiện và thu thập đơn giản mang tính quyết định. Nhưng những nhược điểm ở trên, trong số những thứ khác, là lý do phần lớn các triển khai JVM sẽ sử dụng một GC dựa trên thế hệ, theo dõi.


1
Điều buồn cười là Apple đã chuyển sang ARC chính xác bởi vì họ thấy rằng, trên thực tế, nó vượt trội hơn rất nhiều so với các GC khác (đặc biệt là các thế hệ cụ thể). Công bằng mà nói, điều này chủ yếu đúng trên các nền tảng bị hạn chế bộ nhớ (iPhone). Nhưng tôi phản bác lại tuyên bố của bạn rằng, ARC ARC không phải là không tưởng như trước tiên có vẻ như bằng cách nói rằng các GC thế hệ (và không xác định) khác không phải là không tưởng như trước tiên: Phá hủy xác định có lẽ là một lựa chọn tốt hơn trong đại đa số các kịch bản.
Konrad Rudolph

3
@KonradRudolph mặc dù tôi cũng là một fan hâm mộ của sự hủy diệt mang tính quyết định, tôi không nghĩ rằng lựa chọn tốt hơn trong phần lớn các kịch bản mà nắm giữ. Nó chắc chắn là một lựa chọn tốt hơn khi độ trễ hoặc bộ nhớ quan trọng hơn thông lượng trung bình và đặc biệt khi logic khá đơn giản. Nhưng nó không giống như không có nhiều ứng dụng phức tạp đòi hỏi nhiều tài liệu tham khảo theo chu kỳ, v.v. và yêu cầu hoạt động trung bình nhanh, nhưng không thực sự quan tâm đến độ trễ và có nhiều bộ nhớ khả dụng. Đối với những điều này, thật đáng nghi ngờ nếu ARC là một ý tưởng tốt.
leftaroundabout

1
@leftaroundabout Trong hầu hết các kịch bản, tình huống, thông lượng và áp lực bộ nhớ không phải là một nút cổ chai vì vậy nó cũng không thành vấn đề. Ví dụ của bạn là một kịch bản cụ thể. Cấp, nó không phải là cực kỳ hiếm nhưng tôi sẽ không đi xa đến mức tuyên bố rằng nó phổ biến hơn các kịch bản khác trong đó ARC phù hợp hơn. Hơn nữa, ARC có thể đối phó với các chu kỳ tốt. Nó chỉ cần một số can thiệp đơn giản, thủ công của lập trình viên. Điều này làm cho nó ít lý tưởng hơn nhưng hầu như không phải là một công cụ thỏa thuận. Tôi cho rằng quyết toán quyết định là một đặc điểm quan trọng hơn nhiều so với bạn giả vờ.
Konrad Rudolph

3
@KonradRudolph Nếu ARC yêu cầu một số chương trình can thiệp thủ công đơn giản, thì nó không xử lý theo chu kỳ. Nếu bạn bắt đầu sử dụng nhiều danh sách liên kết đôi, thì ARC sẽ phân bổ thành cấp phát bộ nhớ thủ công. Nếu bạn có các biểu đồ lớn tùy ý, ARC buộc bạn phải viết một trình thu gom rác. Đối số của GC sẽ là các tài nguyên cần hủy không phải là công việc của hệ thống con bộ nhớ và để theo dõi tương đối ít trong số chúng, chúng cần được hoàn thiện một cách xác định thông qua một số can thiệp thủ công đơn giản của lập trình viên.
prosfilaes

2
@KonradRudolph ARC và chu trình cơ bản dẫn đến rò rỉ bộ nhớ nếu chúng không được xử lý thủ công. Trong các hệ thống đủ phức tạp, rò rỉ lớn có thể xảy ra nếu ví dụ một số đối tượng được lưu trữ trên bản đồ lưu trữ một tham chiếu đến bản đồ đó, một thay đổi có thể được thực hiện bởi một lập trình viên không phụ trách các phần tạo mã và phá hủy bản đồ đó. Các biểu đồ lớn tùy ý không có nghĩa là các con trỏ bên trong không mạnh, mà các mục được liên kết biến mất sẽ ổn. Cho dù việc xử lý một số rò rỉ bộ nhớ có ít vấn đề hơn là phải đóng các tệp theo cách thủ công, tôi sẽ không nói, nhưng đó là sự thật.
prosfilaes

5

Java không xác định chính xác khi nào đối tượng được thu thập bởi vì điều đó cho phép triển khai tự do chọn cách xử lý bộ sưu tập rác.

Có nhiều cơ chế thu gom rác khác nhau, nhưng các cơ chế đảm bảo rằng bạn có thể thu thập một đối tượng ngay lập tức gần như hoàn toàn dựa trên việc đếm tham chiếu (tôi không biết về bất kỳ thuật toán nào phá vỡ xu hướng này). Đếm tham chiếu là một công cụ mạnh mẽ, nhưng nó có chi phí duy trì số tham chiếu. Trong mã singlethreaded, không có gì khác hơn là tăng và giảm, do đó, việc gán một con trỏ có thể tốn chi phí theo thứ tự gấp 3 lần mã được tham chiếu so với mã được tính không tham chiếu (nếu trình biên dịch có thể nướng mọi thứ xuống máy mã).

Trong mã đa luồng, chi phí cao hơn. Nó hoặc yêu cầu tăng / giảm nguyên tử hoặc khóa, cả hai đều có thể đắt tiền. Trên một bộ xử lý hiện đại, một hoạt động nguyên tử có thể đắt hơn 20 lần so với một hoạt động đăng ký đơn giản (rõ ràng thay đổi từ bộ xử lý đến bộ xử lý). Điều này có thể làm tăng chi phí.

Vì vậy, với điều này, chúng ta có thể xem xét sự đánh đổi được thực hiện bởi một số mô hình.

  • Objective-C tập trung vào ARC - đếm tham chiếu tự động. Cách tiếp cận của họ là sử dụng tính tham khảo cho tất cả mọi thứ. Không có phát hiện chu kỳ (mà tôi biết), vì vậy các lập trình viên dự kiến ​​sẽ ngăn chặn các chu kỳ xảy ra, làm mất thời gian phát triển. Lý thuyết của họ là các con trỏ không được chỉ định thường xuyên và trình biên dịch của chúng có thể xác định các tình huống trong đó số lượng tham chiếu tăng / giảm không thể làm cho một đối tượng chết và loại bỏ hoàn toàn các mức tăng / giảm đó. Vì vậy, họ giảm thiểu chi phí đếm tham khảo.

  • CPython sử dụng cơ chế lai. Họ sử dụng số lượng tham chiếu, nhưng họ cũng có một trình thu gom rác xác định các chu kỳ và giải phóng chúng. Điều này cung cấp lợi ích của cả hai thế giới, với chi phí của cả hai phương pháp. CPython phải duy trì số lượng tham chiếu làm sổ sách để phát hiện chu kỳ. CPython thoát khỏi điều này theo hai cách. Cái nắm tay là CPython thực sự không đa luồng. Nó có một khóa được gọi là GIL giới hạn đa luồng. Điều này có nghĩa là CPython có thể sử dụng các mức tăng / giảm bình thường thay vì tăng nguyên tử, nhanh hơn nhiều. CPython cũng được hiểu, có nghĩa là các hoạt động như gán cho một biến đã có một số hướng dẫn thay vì chỉ 1. Chi phí bổ sung để thực hiện tăng / giảm, được thực hiện nhanh chóng trong mã C, ít xảy ra sự cố vì chúng tôi ' đã trả chi phí này.

  • Java đi xuống cách tiếp cận không đảm bảo một hệ thống đếm tham chiếu nào cả. Thật vậy, đặc tả không nói về cách các đối tượng được quản lý ngoài việc sẽ có một hệ thống quản lý lưu trữ tự động. Tuy nhiên, đặc điểm kỹ thuật cũng gợi ý mạnh mẽ cho giả định rằng đây sẽ là rác được thu thập theo cách xử lý các chu kỳ. Bằng cách không chỉ định khi các đối tượng hết hạn, java có quyền tự do sử dụng các trình thu thập mà không lãng phí thời gian tăng / giảm thời gian. Thật vậy, các thuật toán thông minh như người thu gom rác thế hệ thậm chí có thể xử lý nhiều trường hợp đơn giản mà không cần nhìn vào dữ liệu đang được thu hồi (họ chỉ phải xem dữ liệu vẫn đang được tham chiếu).

Vì vậy, chúng ta có thể thấy cả ba người này đã phải đánh đổi. Sự đánh đổi nào là tốt nhất phụ thuộc rất lớn vào bản chất của cách sử dụng ngôn ngữ.


4

Mặc dù finalizeđược hỗ trợ dựa trên GC của Java, bộ sưu tập rác ở lõi của nó không quan tâm đến các vật thể chết, mà là những vật thể sống. Trên một số hệ thống GC (có thể bao gồm một số triển khai Java), điều duy nhất phân biệt một bó bit đại diện cho một đối tượng từ một kho lưu trữ không được sử dụng cho bất cứ điều gì có thể là sự tồn tại của các tham chiếu đến trước đây. Trong khi các đối tượng có bộ hoàn thiện được thêm vào một danh sách đặc biệt, các đối tượng khác có thể không có bất cứ nơi nào trong vũ trụ nói rằng bộ lưu trữ của chúng được liên kết với một đối tượng ngoại trừ các tham chiếu được giữ trong mã người dùng. Khi tham chiếu cuối cùng như vậy được ghi đè, mẫu bit trong bộ nhớ sẽ ngay lập tức không còn được nhận dạng như một đối tượng, cho dù có bất kỳ thứ gì trong vũ trụ nhận thức được điều đó hay không.

Mục đích của việc thu gom rác không phải là để tiêu diệt các đối tượng mà không có tài liệu tham khảo nào tồn tại mà là để thực hiện ba điều:

  1. Vô hiệu hóa các tham chiếu yếu xác định các đối tượng không có bất kỳ tham chiếu nào có thể truy cập mạnh được liên kết với chúng.

  2. Tìm kiếm danh sách các đối tượng của hệ thống với bộ hoàn thiện để xem liệu có bất kỳ đối tượng nào không có bất kỳ tài liệu tham khảo nào có thể truy cập mạnh liên quan đến chúng không.

  3. Xác định và hợp nhất các vùng lưu trữ không được sử dụng bởi bất kỳ đối tượng nào.

Lưu ý rằng mục tiêu chính của GC là # 3 và càng chờ đợi lâu trước khi thực hiện nó, càng có nhiều cơ hội hợp nhất thì càng có nhiều cơ hội. Thật hợp lý khi làm số 3 trong trường hợp người ta sẽ sử dụng ngay cho việc lưu trữ, nhưng nếu không thì sẽ có ý nghĩa hơn để trì hoãn nó.


5
Thật ra, gc chỉ có một mục tiêu: Mô phỏng bộ nhớ vô hạn. Tất cả mọi thứ bạn đặt tên như một mục tiêu là một sự không hoàn hảo trong sự trừu tượng hóa hoặc một chi tiết thực hiện.
Ded repeatator

@Ded repeatator: Tài liệu tham khảo yếu cung cấp ngữ nghĩa hữu ích không thể đạt được nếu không có sự trợ giúp của GC.
supercat

Chắc chắn, tài liệu tham khảo yếu có ngữ nghĩa hữu ích. Nhưng liệu những ngữ nghĩa đó có cần thiết nếu mô phỏng tốt hơn không?
Ded repeatator

@Ded repeatator: Có. Xem xét một bộ sưu tập xác định cách cập nhật sẽ tương tác với liệt kê. Một bộ sưu tập như vậy có thể cần giữ các tài liệu tham khảo yếu cho bất kỳ điều tra viên trực tiếp nào. Trong một hệ thống bộ nhớ không giới hạn, một bộ sưu tập được lặp đi lặp lại sẽ có danh sách các điều tra viên quan tâm phát triển mà không bị ràng buộc. Bộ nhớ cần thiết cho danh sách đó sẽ không phải là vấn đề, nhưng thời gian cần thiết để lặp qua nó sẽ làm giảm hiệu suất hệ thống. Thêm GC có thể có nghĩa là sự khác biệt giữa thuật toán O (N) và O (N ^ 2).
supercat

2
Tại sao bạn muốn thông báo cho các điều tra viên, thay vì thêm vào danh sách và để họ tự tìm kiếm khi họ được sử dụng? Và bất kỳ chương trình nào tùy thuộc vào rác được xử lý kịp thời thay vì phụ thuộc vào áp lực bộ nhớ đang sống trong tình trạng tội lỗi, nếu nó di chuyển cả.
Ded repeatator

4

Hãy để tôi đề nghị viết lại và khái quát hóa câu hỏi của bạn:

Tại sao Java không đảm bảo mạnh mẽ về quy trình GC của nó?

Với ý nghĩ đó, hãy lướt nhanh qua các câu trả lời ở đây. Có bảy cho đến nay (không tính cái này), với khá nhiều chủ đề bình luận.

Đó là câu trả lời của bạn.

GC là khó. Có rất nhiều cân nhắc, rất nhiều sự đánh đổi khác nhau, và cuối cùng, rất nhiều cách tiếp cận rất khác nhau. Một số trong những cách tiếp cận đó làm cho nó khả thi đối với một đối tượng ngay khi không cần thiết; những người khác thì không. Bằng cách giữ cho hợp đồng lỏng lẻo, Java cung cấp cho người triển khai nhiều tùy chọn hơn.

Tất nhiên, có một sự đánh đổi ngay cả trong quyết định đó: bằng cách giữ cho hợp đồng lỏng lẻo, Java chủ yếu * lấy đi khả năng của các lập trình viên dựa vào các hàm hủy. Đây là điều mà các lập trình viên C ++ đặc biệt thường bỏ lỡ ([cần dẫn nguồn];)), vì vậy nó không phải là một sự đánh đổi không đáng kể. Tôi chưa thấy một cuộc thảo luận nào về quyết định meta cụ thể đó, nhưng có lẽ những người Java đã quyết định rằng lợi ích của việc có nhiều tùy chọn GC hơn nhiều so với lợi ích của việc có thể nói cho các lập trình viên biết chính xác khi nào một đối tượng sẽ bị phá hủy.


* Có finalizephương pháp, nhưng vì nhiều lý do nằm ngoài phạm vi của câu trả lời này, thật khó và không nên dựa vào nó.


3

Có hai chiến lược xử lý bộ nhớ khác nhau mà không có mã rõ ràng được viết bởi nhà phát triển: Thu gom rác và đếm tham chiếu.

Bộ sưu tập rác có lợi thế là nó "hoạt động" trừ khi nhà phát triển làm điều gì đó ngu ngốc. Với việc đếm tham chiếu, bạn có thể có các chu kỳ tham chiếu, điều đó có nghĩa là nó "hoạt động" nhưng nhà phát triển đôi khi phải khéo léo. Vì vậy, đó là một điểm cộng cho bộ sưu tập rác.

Khi đếm tham chiếu, đối tượng sẽ biến mất ngay lập tức khi số tham chiếu giảm xuống không. Đó là một lợi thế để đếm tham khảo.

Theo tốc độ, bộ sưu tập rác sẽ nhanh hơn nếu bạn tin rằng người hâm mộ của bộ sưu tập rác và việc đếm tham chiếu sẽ nhanh hơn nếu bạn tin rằng người hâm mộ đếm tham chiếu.

Đó chỉ là hai phương thức khác nhau để đạt được cùng một mục tiêu, Java đã chọn một phương thức, Objective-C đã chọn một phương thức khác (và thêm rất nhiều hỗ trợ trình biên dịch để thay đổi nó từ một công cụ khó khăn thành một công việc ít làm cho các nhà phát triển).

Thay đổi Java từ bộ sưu tập rác sang đếm tham chiếu sẽ là một công việc chính, bởi vì sẽ cần rất nhiều thay đổi mã.

Về lý thuyết, Java có thể đã triển khai một hỗn hợp thu gom rác và đếm tham chiếu: Nếu số tham chiếu là 0, thì đối tượng không thể truy cập được, nhưng không nhất thiết phải theo cách khác. Vì vậy, bạn có thể giữ số tham chiếu và xóa các đối tượng khi số tham chiếu của chúng bằng 0 (và thỉnh thoảng chạy bộ sưu tập rác để bắt các đối tượng trong các chu kỳ tham chiếu không thể truy cập được). Tôi nghĩ rằng thế giới bị chia 50/50 ở những người nghĩ rằng thêm tính tham chiếu vào bộ sưu tập rác là một ý tưởng tồi và những người nghĩ rằng thêm bộ sưu tập rác vào đếm tham chiếu là một ý tưởng tồi. Vì vậy, điều này sẽ không xảy ra.

Vì vậy, Java có thể xóa các đối tượng ngay lập tức nếu số tham chiếu của chúng bằng 0 và xóa các đối tượng trong các chu kỳ không thể truy cập sau đó. Nhưng đó là một quyết định thiết kế và Java đã quyết định chống lại nó.


Với tính năng tham chiếu, việc hoàn thiện là chuyện nhỏ, vì lập trình viên đã quan tâm đến các chu kỳ. Với gc, các chu trình là không đáng kể, nhưng lập trình viên phải cẩn thận với việc hoàn thiện.
Ded repeatator

@Ded repeatator Trong Java, bạn cũng có thể tạo các tham chiếu mạnh đến các đối tượng đang được hoàn thiện ... Trong Objective-C và Swift, một khi số tham chiếu bằng 0, đối tượng sẽ biến mất (trừ khi bạn đặt một vòng lặp vô hạn vào dealloc / deist).
gnasher729

Chỉ cần chú ý trình kiểm tra chính tả ngu ngốc thay thế deinit bằng deist ...
gnasher729

1
Có một lý do hầu hết các lập trình viên ghét sửa lỗi chính tả tự động ... ;-)
Ded repeatator

lol ... Tôi nghĩ rằng thế giới bị chia 0,1 / 0,1 / 99,8 giữa những người nghĩ rằng thêm tính năng tham chiếu vào bộ sưu tập rác là một ý tưởng tồi và những người nghĩ rằng thêm bộ sưu tập rác vào đếm tham chiếu là một ý tưởng tồi và những người tiếp tục đếm ngày cho đến khi bộ sưu tập rác đến vì tấn đó đã bốc mùi trở lại ...
leftaroundabout

1

Tất cả các tranh luận và thảo luận về hiệu năng khác về khó hiểu khi không còn tham chiếu đến một đối tượng là chính xác mặc dù một ý tưởng khác mà tôi nghĩ đáng nói là có ít nhất một JVM (azul) xem xét một cái gì đó như thế này trong đó nó thực hiện gc song song về cơ bản có một luồng vm liên tục kiểm tra các tham chiếu để cố gắng xóa chúng, điều này sẽ hành động không hoàn toàn không giống với những gì bạn đang nói. Về cơ bản, nó sẽ liên tục nhìn xung quanh đống và cố gắng lấy lại bất kỳ bộ nhớ nào không được tham chiếu. Điều này không phải chịu một chi phí hiệu suất rất nhỏ nhưng về cơ bản dẫn đến thời gian GC không hoặc rất ngắn. (Đó là trừ khi kích thước heap liên tục mở rộng vượt quá RAM hệ thống và sau đó Azul bị nhầm lẫn và sau đó có rồng)

TLDR Một cái gì đó giống như vậy tồn tại cho JVM, nó chỉ là một jvm đặc biệt và nó có nhược điểm như bất kỳ thỏa hiệp kỹ thuật nào khác.

Tuyên bố miễn trừ trách nhiệm: Tôi không có mối quan hệ nào với Azul, chúng tôi chỉ sử dụng nó ở một công việc trước đó.


1

Tối đa hóa thông lượng duy trì hoặc giảm thiểu độ trễ gc là trong căng thẳng động, đó có lẽ là lý do phổ biến nhất tại sao GC không xảy ra ngay lập tức. Trong một số hệ thống, như các ứng dụng khẩn cấp 911, không đáp ứng ngưỡng độ trễ cụ thể có thể bắt đầu kích hoạt các quy trình xử lý lỗi trang web. Ở những nơi khác, như một trang web ngân hàng và / hoặc trọng tài, điều quan trọng hơn nhiều là tối đa hóa thông lượng.


0

Tốc độ

Tại sao tất cả những điều này đang diễn ra cuối cùng là vì tốc độ. Nếu bộ xử lý cực kỳ nhanh, hoặc (thực tế) gần với nó, ví dụ: 1.000.000.000.000.000.000.000.000.000.000.000 hoạt động mỗi giây thì bạn có thể có những điều cực kỳ dài và phức tạp xảy ra giữa mỗi nhà khai thác, chẳng hạn như đảm bảo xóa các đối tượng được tham chiếu. Vì số lượng hoạt động mỗi giây hiện tại không đúng và, vì hầu hết các câu trả lời khác giải thích nó thực sự phức tạp và tốn nhiều tài nguyên để tìm ra điều này, bộ sưu tập rác tồn tại để các chương trình có thể tập trung vào những gì chúng thực sự đang cố gắng đạt được trong một cách nhanh chóng.


Chà, tôi chắc chắn rằng chúng ta sẽ tìm thấy nhiều cách thú vị hơn để sử dụng hết các chu kỳ bổ sung hơn thế.
Ded repeatator
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.