Mengapa objek Java tidak dihapus segera setelah mereka tidak lagi direferensikan?

77

Di Jawa, segera setelah sebuah objek tidak lagi memiliki referensi, ia menjadi layak untuk dihapus, tetapi JVM memutuskan kapan objek tersebut benar-benar dihapus. Untuk menggunakan terminologi Objective-C, semua referensi Java secara inheren "kuat". Namun, dalam Objective-C, jika suatu objek tidak lagi memiliki referensi yang kuat, objek tersebut segera dihapus. Mengapa ini tidak terjadi di Jawa?

moonman239
sumber
46
Anda seharusnya tidak peduli ketika objek Java benar-benar dihapus. Ini adalah detail implementasi.
Basile Starynkevitch
154
@BasileStarynkevitch Anda harus benar-benar peduli dan menantang cara kerja sistem / platform Anda. Mengajukan pertanyaan 'bagaimana' dan 'mengapa' adalah salah satu cara terbaik untuk menjadi programmer yang lebih baik (dan, secara umum, orang yang lebih pintar).
Artur Biesiadowski
6
Apa yang dilakukan Objective C ketika ada referensi melingkar? Saya menganggap itu hanya membocorkan mereka?
Mehrdad
45
@ArturBiesiadowksi: Tidak, spesifikasi Java tidak memberi tahu kapan suatu objek dihapus (dan juga, untuk R5RS ). Anda bisa dan mungkin harus mengembangkan program Java Anda seandainya penghapusan itu tidak pernah terjadi (dan untuk proses jangka pendek seperti dunia halo Java, itu memang tidak terjadi). Anda mungkin peduli dengan set objek hidup (atau konsumsi memori), yang merupakan cerita yang berbeda.
Basile Starynkevitch
28
Suatu hari pemula berkata kepada master "Saya punya solusi untuk masalah alokasi kami. Kami akan memberikan setiap alokasi hitungan referensi, dan ketika mencapai nol, kami dapat menghapus objek". Sang master menjawab, "Suatu hari, sang pemula berkata kepada sang master," Saya punya solusi ...
Eric Lippert

Jawaban:

79

Pertama-tama, Java memiliki referensi yang lemah dan kategori upaya terbaik lainnya yang disebut referensi lembut. Referensi lemah vs kuat adalah masalah yang sepenuhnya terpisah dari penghitungan referensi vs pengumpulan sampah.

Kedua, ada pola dalam penggunaan memori yang dapat membuat pengumpulan sampah lebih efisien dalam waktu dengan mengorbankan ruang. Misalnya, objek yang lebih baru jauh lebih mungkin dihapus daripada objek yang lebih lama. Jadi, jika Anda menunggu sedikit di antara sapuan, Anda dapat menghapus sebagian besar memori generasi baru, sambil memindahkan beberapa korban ke penyimpanan jangka panjang. Penyimpanan jangka panjang itu dapat dipindai lebih jarang. Penghapusan langsung melalui manajemen memori manual atau penghitungan referensi jauh lebih rentan terhadap fragmentasi.

Ini seperti perbedaan antara berbelanja bahan pangan satu kali per gaji, dan pergi setiap hari untuk mendapatkan makanan yang cukup untuk satu hari. Satu perjalanan besar Anda akan memakan waktu lebih lama daripada perjalanan kecil individual, tetapi secara keseluruhan Anda akhirnya menghemat waktu dan mungkin uang.

Karl Bielefeldt
sumber
58
Istri seorang programmer mengirimnya ke supermarket. Dia mengatakan kepadanya, "Beli sepotong roti, dan jika Anda melihat beberapa telur, ambil selusin." Si programmer kemudian kembali dengan selusin roti di bawah lengannya.
Neil
7
Saya menyarankan untuk menyebutkan bahwa waktu gc generasi baru umumnya sebanding dengan jumlah objek hidup , sehingga memiliki lebih banyak objek yang dihapus berarti biayanya tidak akan dibayar sama sekali dalam banyak kasus. Hapus semudah membalikkan penunjuk ruang selamat dan secara opsional mem-nolkan seluruh ruang memori dalam satu memset besar (tidak yakin apakah itu dilakukan pada akhir gc atau diamortisasi selama alokasi tlab atau objek itu sendiri dalam jvms saat ini)
Artur Biesiadowski
64
@Neil bukankah seharusnya 13 roti?
JAD
67
"Mati karena satu kesalahan di lorong 7"
joeytwiddle
13
@ JAD Saya akan mengatakan 13, tetapi kebanyakan tidak cenderung untuk mendapatkan itu. ;)
Neil
86

Karena mengetahui sesuatu dengan benar tidak lagi dirujuk tidak mudah. Bahkan tidak mendekati mudah.

Bagaimana jika Anda memiliki dua objek yang saling rujukan? Apakah mereka tinggal selamanya? Memperluas garis pemikiran untuk menyelesaikan struktur data yang sewenang-wenang, dan Anda akan segera melihat mengapa JVM atau pengumpul sampah lainnya dipaksa untuk menggunakan metode yang jauh lebih canggih untuk menentukan apa yang masih dibutuhkan dan apa yang bisa berjalan.

Apa namanya
sumber
7
Atau Anda bisa mengambil pendekatan Python di mana Anda menggunakan penghitungan ulang sebanyak mungkin, menggunakan GC ketika Anda mengharapkan ada dependensi melingkar yang membocorkan memori. Saya tidak mengerti mengapa mereka tidak dapat melakukan penghitungan ulang selain GC?
Mehrdad
27
@Mehrdad Mereka bisa. Tapi mungkin itu akan lebih lambat. Tidak ada yang menghentikan Anda untuk mengimplementasikan ini, tetapi jangan berharap untuk mengalahkan salah satu GC di Hotspot atau OpenJ9.
Josef
21
@ jpmc26 karena jika Anda menghapus objek segera setelah tidak digunakan lagi, probabilitasnya tinggi Anda menghapusnya dalam situasi beban tinggi yang menambah beban lebih banyak. GC dapat berjalan saat ada lebih sedikit beban. Penghitungan referensi itu sendiri adalah overhead kecil untuk setiap referensi. Juga dengan GC Anda sering dapat membuang sebagian besar memori tanpa referensi tanpa menangani objek tunggal.
Josef
33
@ Josef: penghitungan referensi yang tepat juga tidak gratis; pembaruan jumlah referensi memerlukan penambahan / penurunan atom, yang secara mengejutkan mahal , terutama pada arsitektur multicore modern. Dalam CPython itu tidak banyak masalah (CPython sangat lambat dengan sendirinya, dan GIL membatasi kinerja multithread untuk tingkat single-core), tetapi pada bahasa yang lebih cepat yang juga mendukung paralelisme itu bisa menjadi masalah. Ini bukan kesempatan bahwa PyPy menghilangkan penghitungan referensi sepenuhnya dan hanya menggunakan GC.
Matteo Italia
10
@Mehrdad setelah Anda menerapkan referensi Anda menghitung GC untuk Java Saya dengan senang hati akan mengujinya untuk menemukan kasus di mana kinerjanya lebih buruk daripada implementasi GC lainnya.
Josef
45

AFAIK, spesifikasi JVM (ditulis dalam bahasa Inggris) tidak menyebutkan kapan objek (atau nilai) yang tepat harus dihapus, dan membiarkannya untuk implementasi (demikian juga untuk R5RS ). Entah bagaimana itu membutuhkan atau menyarankan seorang pemulung, tetapi menyerahkan detailnya pada implementasinya. Dan juga untuk spesifikasi Java.

Ingat bahwa bahasa pemrograman adalah spesifikasi ( sintaks , semantik , dll ...), bukan implementasi perangkat lunak. Bahasa seperti Java (atau JVM) memiliki banyak implementasi. Spesifikasinya diterbitkan , dapat diunduh (sehingga Anda dapat mempelajarinya) dan ditulis dalam bahasa Inggris. §2.5.3 Tumpukan spesifikasi JVM menyebutkan seorang pemulung:

Penyimpanan tumpukan untuk benda-benda direklamasi oleh sistem manajemen penyimpanan otomatis (dikenal sebagai pengumpul sampah); objek tidak pernah secara eksplisit dialokasikan. Java Virtual Machine tidak mengasumsikan tipe tertentu dari sistem manajemen penyimpanan otomatis

(Penekanan adalah milik saya; Finalisasi BTW disebutkan dalam §12.6 dari spesifikasi Java, dan model memori ada di §17.4 dari spesifikasi Java)

Jadi (di Jawa) Anda seharusnya tidak peduli ketika suatu objek dihapus , dan Anda bisa mengkodekan as-jika itu tidak terjadi (dengan alasan dalam abstraksi di mana Anda mengabaikannya). Tentu saja Anda perlu memperhatikan konsumsi memori dan set objek hidup, yang merupakan pertanyaan yang berbeda . Dalam beberapa kasus sederhana (pikirkan program "hello world") Anda dapat membuktikan - atau meyakinkan diri sendiri - bahwa memori yang dialokasikan agak kecil (misalnya kurang dari satu gigabyte), dan kemudian Anda tidak peduli sama sekali tentang penghapusan objek individual . Dalam lebih banyak kasus, Anda dapat meyakinkan diri Anda bahwa benda itu hidup(atau yang dapat dijangkau, yang merupakan superset - lebih mudah untuk alasan - hidup) tidak pernah melebihi batas yang wajar (dan kemudian Anda benar-benar mengandalkan GC, tetapi Anda tidak peduli bagaimana dan kapan pengumpulan sampah terjadi). Baca tentang kompleksitas ruang .

Saya kira pada beberapa implementasi JVM yang menjalankan program Java berumur pendek seperti hello world one, pengumpul sampah tidak dipicu sama sekali dan tidak ada penghapusan terjadi. AFAIU, perilaku seperti itu sesuai dengan banyak spesifikasi Java.

Sebagian besar implementasi JVM menggunakan teknik penyalinan generasional (setidaknya untuk sebagian besar objek Java, yang tidak menggunakan finalisasi atau referensi lemah ; dan finalisasi tidak dijamin akan terjadi dalam waktu singkat dan dapat ditunda, jadi ini hanya fitur yang bermanfaat bahwa kode Anda tidak boleh sangat bergantung pada) di mana gagasan menghapus objek individual tidak masuk akal (karena blok besar memori - yang berisi zona memori untuk banyak objek -, mungkin beberapa megabyte sekaligus, dapat dilepaskan sekaligus).

Jika spesifikasi JVM mengharuskan setiap objek untuk dihapus sesegera mungkin (atau hanya menempatkan lebih banyak kendala pada penghapusan objek), teknik GC generasi yang efisien akan dilarang, dan perancang Java dan JVM telah bijaksana dalam menghindarinya.

BTW, bisa jadi mungkin JVM naif yang tidak pernah menghapus objek dan tidak melepaskan memori mungkin sesuai dengan spesifikasi (surat, bukan semangat) dan tentu saja dapat menjalankan hal halo dunia dalam praktiknya (perhatikan bahwa sebagian besar program Java yang kecil dan berumur pendek mungkin tidak mengalokasikan lebih dari beberapa gigabytes memori). Tentu saja seperti JVM tidak layak disebut dan hanya hal mainan (seperti yang ini pelaksanaan mallocuntuk C). Lihat Epsilon NoOp GC untuk informasi lebih lanjut. JVM kehidupan nyata adalah perangkat lunak yang sangat kompleks dan memadukan beberapa teknik pengumpulan sampah.

Juga, Java tidak sama dengan JVM, dan Anda memiliki implementasi Java berjalan tanpa JVM (misalnya depan-of-waktu compiler Java, runtime Android ). Dalam beberapa kasus (sebagian besar yang akademis), Anda mungkin membayangkan (disebut teknik "pengumpulan sampah waktu") bahwa program Java tidak mengalokasikan atau menghapus pada saat runtime (misalnya karena kompilator pengoptimal telah cukup pintar untuk hanya menggunakan tumpukan panggilan dan variabel otomatis ).

Mengapa objek Java tidak dihapus segera setelah mereka tidak lagi direferensikan?

Karena spesifikasi Java dan JVM tidak memerlukan itu.


Baca buku pegangan GC untuk informasi lebih lanjut (dan spesifikasi JVM ). Perhatikan bahwa menjadi hidup (atau berguna untuk perhitungan di masa depan) untuk suatu objek adalah properti seluruh program (non-modular).

Objective-C mendukung pendekatan penghitungan referensi untuk manajemen memori . Dan itu juga memiliki jebakan (misalnya programmer Objective-C harus peduli tentang referensi melingkar dengan menjelaskan referensi yang lemah, tetapi JVM menangani referensi melingkar dengan baik dalam praktek tanpa memerlukan perhatian dari programmer Java).

Tidak Ada Peluru Perak dalam pemrograman dan desain bahasa pemrograman (waspadai Masalah Pemutusan ; menjadi objek hidup yang bermanfaat tidak dapat diputuskan secara umum).

Anda mungkin juga membaca SICP , Memprogram Bahasa Pragmatik , Buku Naga , Cincang Dalam Potongan - potongan Kecil dan Sistem Operasi: Tiga Potong Mudah . Mereka bukan tentang Java, tetapi mereka akan membuka pikiran Anda dan harus membantu untuk memahami apa yang harus dilakukan JVM dan bagaimana cara kerjanya secara praktis (dengan bagian lain) di komputer Anda. Anda juga dapat menghabiskan waktu berbulan-bulan (atau beberapa tahun) untuk mempelajari kode sumber kompleks dari implementasi JVM open source yang ada (seperti OpenJDK , yang memiliki beberapa juta baris kode sumber).

Basile Starynkevitch
sumber
20
"Mungkin saja JVM naif yang tidak pernah menghapus objek dan tidak melepaskan memori mungkin sesuai dengan spesifikasi" Ini tentu saja sesuai dengan spesifikasi! Java 11 sebenarnya menambahkan pengumpul sampah no-op untuk, antara lain, program yang sangat berumur pendek.
Michael
6
"Anda seharusnya tidak peduli ketika suatu objek dihapus" Tidak setuju. Untuk satu, Anda harus tahu bahwa RAII bukan pola yang layak lagi, dan bahwa Anda tidak dapat bergantung pada finalizemanajemen sumber daya apa pun (dari filehandles, koneksi db, sumber daya gpu, dll.).
Alexander
4
@Michael Sangat masuk akal untuk pemrosesan batch dengan plafon memori yang digunakan. OS hanya bisa mengatakan "semua memori yang digunakan oleh program ini sekarang hilang!" setelah semua, yang agak cepat. Memang, banyak program dalam C ditulis seperti itu, terutama di dunia Unix awal. Pascal memiliki "reset stack / heap pointer yang mengerikan dan mengerikan ke pos pemeriksaan yang disimpan sebelumnya" yang memungkinkan Anda melakukan banyak hal yang sama, meskipun itu cukup tidak aman - tandai, mulai sub-tugas, atur ulang.
Luaan
6
@Alexander secara umum di luar C ++ (dan beberapa bahasa yang secara sengaja diturunkan darinya), dengan asumsi RAII akan bekerja berdasarkan finalizers saja adalah anti-pola, yang harus diperingatkan dan diganti dengan blok kontrol sumber daya eksplisit. Inti dari GC adalah bahwa seumur hidup dan sumber daya dipisahkan, setelah semua.
Leushenko
3
@ Leushenko Saya akan sangat tidak setuju bahwa "seumur hidup dan sumber daya dipisahkan" adalah "seluruh titik" dari GC. Ini adalah harga negatif yang Anda bayar untuk poin utama GC: manajemen memori yang mudah dan aman. "Dengan asumsi RAII akan bekerja berdasarkan finalizers saja adalah anti-pola" Di Jawa? Mungkin. Tetapi tidak dalam CPython, Rust, Swift, atau Objective C. "diperingatkan dan diganti dengan blok kontrol sumber daya eksplisit" Tidak, ini sangat terbatas. Objek yang mengelola sumber daya melalui RAII memberi Anda pegangan untuk melewati kehidupan yang dilingkari. Blok coba-dengan-sumber daya terbatas pada cakupan tunggal.
Alexander
23

Untuk menggunakan terminologi Objective-C, semua referensi Java secara inheren "kuat".

Itu tidak benar - Java memang memiliki referensi lemah dan lunak, meskipun ini diterapkan pada tingkat objek daripada sebagai kata kunci bahasa.

Di Objective-C, jika suatu objek tidak lagi memiliki referensi yang kuat, objek tersebut segera dihapus.

Itu juga belum tentu benar - beberapa versi Objective C memang menggunakan pengumpul sampah generasi. Versi lain tidak memiliki pengumpulan sampah sama sekali.

Memang benar bahwa versi yang lebih baru dari Objective C menggunakan penghitungan referensi otomatis (ARC) daripada GC berbasis jejak, dan ini (sering) menghasilkan objek yang "dihapus" ketika penghitungan referensi itu mencapai nol. Namun, perhatikan bahwa implementasi JVM juga bisa memenuhi syarat dan bekerja dengan cara ini (heck, bisa sesuai dan tidak memiliki GC sama sekali.)

Jadi mengapa sebagian besar implementasi JVM tidak melakukan ini, dan alih-alih menggunakan algoritme GC berbasis jejak?

Sederhananya, ARC tidak utopis seperti yang terlihat pertama kali:

  • Anda harus menambah atau mengurangi penghitung setiap kali referensi disalin, dimodifikasi, atau keluar dari ruang lingkup, yang membawa overhead kinerja yang jelas.
  • ARC tidak dapat dengan mudah menghapus referensi siklus, karena mereka semua memiliki referensi satu sama lain, sehingga jumlah referensi mereka tidak pernah mencapai nol.

ARC memang memiliki kelebihan tentu saja - mudah diterapkan dan pengumpulannya bersifat deterministik. Namun kelemahan di atas, antara lain, adalah alasan mayoritas implementasi JVM akan menggunakan GC berbasis generasi.

berry120
sumber
1
Yang lucu adalah bahwa Apple beralih ke ARC justru karena mereka melihat bahwa, dalam praktiknya, itu jauh lebih baik dari GC lain (khususnya yang generasi). Agar adil, ini sebagian besar berlaku pada platform yang dibatasi memori (iPhone). Tapi saya akan membantah pernyataan Anda bahwa "ARC tidak utopis seperti yang pertama kali terlihat" dengan mengatakan bahwa GC generasional (dan non-deterministik lainnya) tidak se utopian seperti yang terlihat: Penghancuran deterministik mungkin merupakan pilihan yang lebih baik di Amerika. sebagian besar skenario.
Konrad Rudolph
3
@KonradRudolph meskipun saya agak penggemar kehancuran deterministik juga, saya tidak berpikir "pilihan yang lebih baik di sebagian besar skenario" berlaku. Ini tentu saja pilihan yang lebih baik ketika latensi atau memori lebih penting daripada throughput rata-rata, dan khususnya ketika logikanya cukup sederhana. Tetapi tidak seperti tidak ada banyak aplikasi kompleks yang membutuhkan banyak referensi siklik dll. Dan membutuhkan operasi rata-rata yang cepat, tetapi tidak terlalu peduli dengan latensi dan memiliki banyak memori yang tersedia. Untuk ini, diragukan apakah ARC adalah ide yang bagus.
leftaroundabout
1
@leftaroundabout Dalam "sebagian besar skenario", baik throughput maupun tekanan memori tidak menjadi hambatan sehingga tidak masalah. Contoh Anda adalah satu skenario khusus. Memang, itu tidak terlalu umum tetapi saya tidak akan pergi sejauh mengklaim bahwa itu lebih umum daripada skenario lain di mana ARC lebih cocok. Selain itu, ARC dapat menangani siklus dengan baik. Itu hanya memerlukan beberapa intervensi manual yang sederhana oleh programmer. Ini membuatnya kurang ideal tetapi bukan pemecah kesepakatan. Saya berpendapat bahwa finalisasi deterministik adalah karakteristik yang jauh lebih penting daripada yang Anda bayangkan.
Konrad Rudolph
3
@KonradRudolph Jika ARC memerlukan beberapa intervensi manual sederhana oleh programmer, maka itu tidak berurusan dengan siklus. Jika Anda mulai menggunakan daftar yang ditautkan secara ganda, maka ARC beralih ke alokasi memori manual. Jika Anda memiliki grafik arbitrer besar, ARC memaksa Anda untuk menulis pengumpul sampah. Argumen GC adalah bahwa sumber daya yang membutuhkan penghancuran bukan pekerjaan subsistem memori, dan untuk melacak yang relatif sedikit dari mereka, mereka harus ditentukan secara deterministik melalui beberapa intervensi manual sederhana oleh programmer.
prosfila
2
@KonradRudolph ARC dan siklus pada dasarnya menyebabkan kebocoran memori jika tidak ditangani secara manual. Dalam sistem yang cukup kompleks, kebocoran besar dapat terjadi jika misalnya beberapa objek yang disimpan dalam peta menyimpan referensi ke peta itu, perubahan yang dapat dilakukan oleh seorang programmer yang tidak bertanggung jawab atas bagian-bagian pembuatan kode dan menghancurkan peta itu. Grafik arbitrer besar tidak berarti bahwa pointer internal tidak kuat, bahwa tidak apa-apa jika item yang ditautkan menghilang. Apakah berurusan dengan beberapa kebocoran memori kurang dari masalah daripada harus secara manual menutup file, saya tidak akan mengatakannya, tetapi itu nyata.
prosfila
5

Java tidak menentukan secara pasti kapan objek dikumpulkan karena itu memberikan implementasi kebebasan untuk memilih bagaimana menangani pengumpulan sampah.

Ada banyak mekanisme pengumpulan sampah yang berbeda, tetapi mekanisme yang menjamin Anda dapat mengumpulkan objek dengan segera hampir seluruhnya didasarkan pada penghitungan referensi (saya tidak mengetahui adanya algoritma yang mematahkan tren ini). Penghitungan referensi adalah alat yang ampuh, tetapi harus dibayar dengan mempertahankan jumlah referensi. Dalam kode singlethreaded, itu tidak lebih dari kenaikan dan penurunan, sehingga penunjuk pointer dapat biaya biaya pada urutan 3x sebanyak dalam kode terhitung referensi daripada dalam kode dihitung non-referensi (jika kompiler dapat memanggang semuanya ke mesin kode).

Dalam kode multithreaded, biayanya lebih tinggi. Itu baik panggilan untuk kenaikan / penurunan atom atau kunci, yang keduanya bisa mahal. Pada prosesor modern, operasi atom bisa 20x lebih mahal daripada operasi register sederhana (jelas bervariasi dari prosesor ke prosesor). Ini dapat meningkatkan biaya.

Jadi dengan ini, kita dapat mempertimbangkan pengorbanan yang dibuat oleh beberapa model.

  • Objective-C berfokus pada ARC - penghitungan referensi otomatis. Pendekatan mereka adalah menggunakan penghitungan referensi untuk semuanya. Tidak ada deteksi siklus (yang saya tahu), sehingga programmer diharapkan untuk mencegah siklus terjadi, yang memakan waktu pengembangan. Teorinya adalah bahwa pointer tidak ditugaskan terlalu sering, dan kompiler mereka dapat mengidentifikasi situasi di mana jumlah referensi bertambah / berkurang tidak dapat menyebabkan objek mati, dan mengeliminasi kenaikan / penurunan tersebut sepenuhnya. Dengan demikian mereka meminimalkan biaya penghitungan referensi.

  • CPython menggunakan mekanisme hybrid. Mereka menggunakan jumlah referensi, tetapi mereka juga memiliki pengumpul sampah yang mengidentifikasi siklus dan melepaskan mereka. Ini memberikan manfaat dari kedua dunia, dengan mengorbankan kedua pendekatan. CPython harus mempertahankan jumlah referensi danlakukan pembukuan untuk mendeteksi siklus. CPython lolos dengan ini dalam dua cara. Kepalannya adalah bahwa CPython benar-benar tidak sepenuhnya multithreaded. Ini memiliki kunci yang dikenal sebagai GIL yang membatasi multithreading. Ini berarti CPython dapat menggunakan kenaikan / penurunan normal daripada yang atomik, yang jauh lebih cepat. CPython juga ditafsirkan, yang berarti operasi seperti penugasan ke variabel sudah mengambil beberapa instruksi daripada hanya 1. Biaya tambahan untuk melakukan penambahan / pengurangan, yang dilakukan dengan cepat dalam kode C, kurang menjadi masalah karena kami sudah membayar biaya ini.

  • Java menurunkan pendekatan tidak menjamin sistem terhitung referensi sama sekali. Memang spesifikasinya tidak mengatakan apa - apa tentang bagaimana objek dikelola selain bahwa akan ada sistem manajemen penyimpanan otomatis. Namun, spesifikasi juga sangat mengisyaratkan asumsi bahwa ini akan menjadi sampah yang dikumpulkan dengan cara yang menangani siklus. Dengan tidak menentukan kapan objek kedaluwarsa, java mendapatkan kebebasan untuk menggunakan kolektor yang tidak membuang waktu bertambah / berkurang. Memang, algortihms pintar seperti pengumpul sampah generasi bahkan dapat menangani banyak kasus sederhana tanpa melihat data yang sedang direklamasi (mereka hanya perlu melihat data yang masih dirujuk).

Jadi kita bisa melihat masing-masing dari ketiganya harus melakukan tradeoff. Pengorbanan mana yang terbaik sangat tergantung pada sifat bagaimana bahasa itu dimaksudkan untuk digunakan.

Cort Ammon
sumber
4

Meskipun finalizedidukung piggy ke GC Jawa, pengumpulan sampah pada intinya tidak tertarik pada benda mati, tetapi yang hidup. Pada beberapa sistem GC (mungkin termasuk beberapa implementasi Java), satu-satunya hal yang membedakan sekelompok bit yang mewakili objek dari sekelompok penyimpanan yang tidak digunakan untuk apa pun mungkin adalah adanya referensi ke yang sebelumnya. Sementara objek dengan finalizer ditambahkan ke daftar khusus, objek lain mungkin tidak memiliki apa pun di alam semesta yang mengatakan penyimpanannya dikaitkan dengan objek kecuali untuk referensi yang disimpan dalam kode pengguna. Ketika referensi seperti terakhir ditimpa, pola bit dalam memori akan segera berhenti dikenali sebagai objek, apakah ada sesuatu di alam semesta yang menyadarinya atau tidak.

Tujuan pengumpulan sampah bukan untuk menghancurkan objek yang tidak memiliki referensi, tetapi untuk mencapai tiga hal:

  1. Validasi referensi yang lemah yang mengidentifikasi objek yang tidak memiliki referensi yang sangat terjangkau yang terkait dengannya.

  2. Cari daftar objek sistem dengan finalizer untuk melihat apakah ada di antara mereka yang tidak memiliki referensi yang sangat terjangkau yang terkait dengannya.

  3. Identifikasi dan konsolidasi wilayah penyimpanan yang tidak digunakan oleh objek apa pun.

Perhatikan bahwa tujuan utama GC adalah # 3, dan semakin lama menunggu sebelum melakukannya, semakin banyak peluang untuk konsolidasi yang mungkin dimiliki. Masuk akal untuk melakukan # 3 dalam kasus di mana seseorang akan segera menggunakan untuk penyimpanan, tetapi sebaliknya lebih masuk akal untuk menunda itu.

supercat
sumber
5
Sebenarnya, gc hanya memiliki satu tujuan: Mensimulasikan memori tanpa batas. Segala sesuatu yang Anda namakan sebagai tujuan adalah ketidaksempurnaan dalam abstraksi atau detail implementasi.
Deduplicator
@Deduplicator: Referensi yang lemah menawarkan semantik yang bermanfaat yang tidak dapat dicapai tanpa bantuan GC.
supercat
Tentu, referensi yang lemah memiliki semantik yang bermanfaat. Tapi apakah semantik itu diperlukan jika simulasi lebih baik?
Deduplicator
@Dupuplikator: Ya. Pertimbangkan koleksi yang menentukan bagaimana pembaruan akan berinteraksi dengan enumerasi. Koleksi seperti itu mungkin perlu memiliki referensi yang lemah untuk enumerator langsung. Dalam sistem memori tak terbatas, koleksi yang diulang berulang kali akan membuat daftar pencacah yang tertarik bertambah tanpa batas. Memori yang diperlukan untuk daftar itu tidak akan menjadi masalah, tetapi waktu yang diperlukan untuk mengulanginya akan menurunkan kinerja sistem. Menambahkan GC dapat berarti perbedaan antara algoritma O (N) dan O (N ^ 2).
supercat
2
Mengapa Anda ingin memberi tahu pencacah, alih-alih menambahkan ke daftar dan membiarkan mereka mencari sendiri ketika mereka digunakan? Dan program apa pun yang bergantung pada sampah yang diproses tepat waktu alih-alih bergantung pada tekanan ingatan, tetap hidup dalam keadaan dosa, jika ia bergerak sama sekali.
Deduplicator
4

Izinkan saya menyarankan penulisan ulang dan generalisasi pertanyaan Anda:

Mengapa Java tidak membuat jaminan kuat tentang proses GC-nya?

Dengan mengingat hal itu, gulir cepat jawabannya di sini. Ada tujuh sejauh ini (tidak termasuk yang satu ini), dengan beberapa utas komentar.

Itu jawaban kamu.

GC sulit. Ada banyak pertimbangan, banyak pertukaran yang berbeda, dan, pada akhirnya, banyak pendekatan yang sangat berbeda. Beberapa pendekatan tersebut memungkinkan untuk GC suatu objek segera setelah itu tidak diperlukan; yang lain tidak. Dengan menjaga kontrak tetap longgar, Java memberi pelaksana lebih banyak opsi.

Ada tradeoff bahkan dalam keputusan itu, tentu saja: dengan menjaga kontrak tetap longgar, sebagian besar Java * menghilangkan kemampuan programmer untuk bergantung pada destruktor. Ini adalah sesuatu yang sering dilewatkan oleh programmer C ++ ([rujukan?];)), Jadi itu bukan tradeoff yang tidak signifikan. Saya belum melihat diskusi tentang meta-keputusan tertentu, tetapi mungkin orang-orang Jawa memutuskan bahwa manfaat memiliki lebih banyak opsi GC melebihi manfaat untuk memberitahu programmer tepat ketika suatu objek akan dihancurkan.


* Ada finalizemetode, tetapi karena berbagai alasan yang di luar cakupan untuk jawaban ini, sulit dan bukan ide yang baik untuk mengandalkannya.

yshavit
sumber
3

Ada dua strategi berbeda dalam menangani memori tanpa kode eksplisit yang ditulis oleh pengembang: Pengumpulan sampah, dan penghitungan referensi.

Pengumpulan sampah memiliki keuntungan bahwa ia "berfungsi" kecuali jika pengembang melakukan sesuatu yang bodoh. Dengan penghitungan referensi, Anda dapat memiliki siklus referensi, yang berarti bahwa itu "berfungsi" tetapi pengembang terkadang harus pintar. Jadi itu nilai tambah untuk pengumpulan sampah.

Dengan penghitungan referensi, objek akan hilang segera ketika penghitungan referensi turun ke nol. Itu keuntungan untuk penghitungan referensi.

Secara cepat, pengumpulan sampah lebih cepat jika Anda yakin para penggemar pengumpulan sampah, dan penghitungan referensi lebih cepat jika Anda yakin para penggemar penghitungan referensi.

Hanya ada dua metode berbeda untuk mencapai tujuan yang sama, Java memilih satu metode, Objective-C memilih yang lain (dan menambahkan banyak dukungan kompiler untuk mengubahnya dari yang menyusahkan menjadi sesuatu yang sedikit bekerja untuk pengembang).

Mengubah Jawa dari pengumpulan sampah ke penghitungan referensi akan menjadi pekerjaan besar, karena banyak perubahan kode akan diperlukan.

Secara teori, Jawa bisa menerapkan campuran pengumpulan sampah dan penghitungan referensi: Jika penghitungan referensi adalah 0, maka objek tersebut tidak dapat dijangkau, tetapi tidak harus sebaliknya. Jadi, Anda dapat menyimpan jumlah referensi dan menghapus objek ketika jumlah referensi mereka nol (dan kemudian menjalankan pengumpulan sampah dari waktu ke waktu untuk menangkap objek dalam siklus referensi yang tidak terjangkau). Saya pikir dunia terbagi 50/50 pada orang yang berpikir bahwa menambahkan penghitungan referensi ke pengumpulan sampah adalah ide yang buruk, dan orang yang berpikir bahwa menambahkan pengumpulan sampah ke penghitungan referensi adalah ide yang buruk. Jadi ini tidak akan terjadi.

Jadi Java dapat menghapus objek dengan segera jika jumlah referensi mereka menjadi nol, dan menghapus objek dalam siklus yang tidak terjangkau kemudian. Tapi itu keputusan desain, dan Java memutuskan untuk tidak melakukannya.

gnasher729
sumber
Dengan penghitungan referensi, finalisasi itu sepele, karena programmer mengatur siklus. Dengan gc, siklus itu sepele, tetapi programmer harus berhati-hati dalam menyelesaikannya.
Deduplicator
@Dupuplikator Di Jawa, dimungkinkan juga untuk membuat referensi yang kuat ke objek yang diselesaikan ... Dalam Objective-C dan Swift, setelah jumlah referensi nol, objek akan hilang (kecuali jika Anda menempatkan loop tak terbatas ke dealloc / deist).
gnasher729
Hanya memperhatikan pemeriksa ejaan yang bodoh menggantikan deinit dengan deist ...
gnasher729
1
Ada alasan kebanyakan programmer membenci koreksi ejaan otomatis ... ;-)
Deduplicator
lol ... Saya pikir dunia terbagi 0,1 / 0,1 / 99,8 antara orang-orang yang berpikir bahwa menambahkan penghitungan referensi ke pengumpulan sampah adalah ide yang buruk, dan orang-orang yang berpikir bahwa menambahkan pengumpulan sampah ke penghitungan referensi adalah ide yang buruk, dan orang-orang yang terus berhitung hari sampai pengumpulan sampah tiba karena ton itu sudah berbau lagi ...
leftaroundtentang
1

Semua argumen kinerja lain dan diskusi tentang kesulitan memahami ketika tidak ada lagi referensi ke objek benar meskipun satu ide lain yang menurut saya layak disebutkan adalah bahwa setidaknya ada satu JVM (azul) yang menganggap sesuatu seperti ini karena mengimplementasikan paralel gc yang intinya memiliki thread vm yang terus-menerus memeriksa referensi untuk mencoba menghapusnya yang akan bertindak tidak sepenuhnya berbeda dari apa yang Anda bicarakan. Pada dasarnya ia akan terus melihat-lihat tumpukan dan mencoba untuk mendapatkan kembali memori yang tidak direferensikan. Ini memang menimbulkan biaya kinerja yang sangat sedikit tetapi pada dasarnya mengarah ke nol atau waktu GC sangat singkat. (Itu kecuali ukuran tumpukan terus-menerus melebihi sistem RAM dan kemudian Azul menjadi bingung dan kemudian ada naga)

TLDR Sesuatu seperti itu agak ada untuk JVM itu hanya jvm khusus dan memiliki kelemahan seperti kompromi teknik lainnya.

Penafian: Saya tidak memiliki ikatan dengan Azul kami hanya menggunakannya di pekerjaan sebelumnya.

ford prefek
sumber
1

Memaksimalkan throughput berkelanjutan atau meminimalkan latensi gc berada dalam ketegangan dinamis, yang mungkin merupakan alasan paling umum mengapa GC tidak terjadi segera. Di beberapa sistem, seperti 911 aplikasi darurat, tidak memenuhi ambang latensi tertentu dapat mulai memicu proses kegagalan situs. Di tempat lain, seperti situs perbankan dan / atau arbitrase, jauh lebih penting untuk memaksimalkan throughput.

barmid
sumber
0

Kecepatan

Mengapa semua ini terjadi pada akhirnya karena kecepatan. Jika prosesor sangat cepat, atau (praktis) dekat dengannya, mis. 1.000.000.000.000.000.000.000.000.000.000.000.000.000.000 per detik maka Anda dapat memiliki hal-hal yang sangat panjang dan rumit terjadi di antara masing-masing operator, seperti memastikan objek yang dirujuk dihapus. Karena jumlah operasi per detik saat ini tidak benar dan, karena sebagian besar jawaban lain menjelaskan itu sebenarnya rumit dan sumber daya intensif untuk mencari tahu ini, pengumpulan sampah ada sehingga program dapat fokus pada apa yang sebenarnya mereka capai dalam suatu cara cepat.

Michael Durrant
sumber
Yah, saya yakin kita akan menemukan cara yang lebih menarik untuk menggunakan siklus tambahan dari itu.
Deduplicator