Jadi saya berpikir tentang cara kerja pengumpul sampah dan saya memikirkan masalah yang menarik. Agaknya pengumpul sampah harus melintasi semua struktur dengan cara yang sama. Mereka tidak dapat mengetahui cuaca mereka melintasi daftar tertaut atau pohon seimbang atau apa pun. Mereka juga tidak dapat menggunakan terlalu banyak memori dalam pencarian mereka. Satu cara yang mungkin, dan satu-satunya cara saya bisa berpikir untuk melintasi SEMUA struktur, mungkin hanya dengan melintasi semuanya secara rekursif seperti yang Anda lakukan pada pohon biner. Namun ini akan memberikan stack overflow pada daftar tertaut atau bahkan hanya pohon biner yang kurang seimbang. Tetapi semua sampah yang dikumpulkan bahasa yang pernah saya gunakan tampaknya tidak punya masalah dalam menangani kasus-kasus seperti itu.
Dalam buku naga itu menggunakan semacam antrian "Tidak dipindai". Pada dasarnya daripada melintasi struktur secara rekursif itu hanya menambahkan hal-hal yang perlu ditandai juga antrian dan kemudian untuk setiap hal yang tidak ditandai pada akhirnya dihapus. Tapi bukankah antrian ini menjadi sangat besar?
Jadi, bagaimana pengumpul sampah melintasi struktur sewenang-wenang? Bagaimana teknik traversal ini menghindari overflow?
Jawaban:
Perhatikan bahwa saya bukan ahli pengumpulan sampah. Jawaban ini hanya memberikan contoh teknik. Saya tidak mengklaim bahwa ini adalah gambaran representatif dari teknik pengumpulan sampah.
Antrian yang tidak dipindai adalah pilihan umum. Antrian bisa menjadi besar - berpotensi sebesar struktur data terdalam. Antrian biasanya disimpan secara eksplisit, bukan di tumpukan thread pengumpulan sampah.
Setelah semua kecuali satu anak dari sebuah node telah dipindai, node tersebut dapat dihapus dari antrian yang tidak dipindai. Ini pada dasarnya adalah optimasi panggilan ekor. Pengumpul sampah dapat menyertakan heuristik untuk mencoba memindai anak yang paling dalam dari simpul yang terakhir; misalnya GC untuk Lisp harus memindai
car
daricons
sebelumcdr
.Salah satu cara untuk menghindari agar tidak ada antrian yang tidak terpindai adalah dengan memodifikasi pointer, membuat anak sementara menunjuk ke orang tua. Ini adalah teknik traversal pohon memori konstan yang digunakan dalam konteks selain pengumpul sampah. Kelemahan dari teknik ini adalah bahwa sementara GC melintasi struktur data, struktur data tidak valid, sehingga GC harus menghentikan dunia. Ini bukan pemecah kesepakatan: banyak pemulung yang memasukkan fase yang menghentikan dunia, selain fase yang tidak tapi bisa kehilangan sampah sebagai hasilnya.
sumber
Singkatnya : Pengumpul sampah tidak menggunakan rekursi. Mereka hanya mengontrol pelacakan dengan melacak dua set dasarnya (yang dapat menggabungkan). Urutan penelusuran dan pemrosesan sel tidak relevan, yang memberikan kebebasan implementasi yang cukup besar untuk mewakili set. Karenanya ada banyak solusi yang sebenarnya sangat murah dalam penggunaan memori. Ini penting karena GC disebut tepat ketika tumpukan kehabisan memori. Hal-hal agak berbeda dengan memori virtual besar, karena halaman baru dapat dengan mudah dialokasikan, dan musuh bukanlah kekurangan ruang, tetapi kurangnya data lokalitas .
Saya berasumsi Anda sedang mempertimbangkan melacak pemulung, bukan penghitungan referensi yang tampaknya tidak berlaku untuk pertanyaan Anda.
Pertanyaannya adalah fokus pada biaya memori pelacakan untuk melacak satu set: set (untuk yang tidak dilacak) dari sel memori yang dapat diakses yang masih mengandung pointer yang belum dilacak. Ini hanya setengah dari masalah memori untuk pengumpulan sampah. GC juga harus melacak set lain: set (untuk dikunjungi) dari semua sel yang telah ditemukan dapat diakses, sehingga dapat mengklaim kembali semua sel lain di akhir proses. Membahas satu dan bukan yang lain masuk akal terbatas, karena mereka mungkin memiliki biaya yang sama, menggunakan solusi yang sama, dan bahkan digabungkan.VU V
Hal pertama yang perlu diperhatikan adalah bahwa semua pelacakan GC mengikuti model abstrak yang sama, berdasarkan eksplorasi sistematis dari sel-sel yang diarahkan dalam memori yang dapat diakses dari program, di mana sel-sel memori adalah simpul dan pointer adalah tepi yang diarahkan. Ini digunakan untuk set berikut:
himpunan (dikunjungi) dari sel-sel yang sudah ditemukan dapat diakses oleh mutator , yaitu program atau algoritma untuk mana GC dilakukan. Himpunan dipartisi menjadi dua himpunan bagian terpisah: ;V V = U ∪ TV V V=U∪T
set (tidak terlacak) dari sel yang dikunjungi dengan pointer yang belum diikuti;U
set (dilacak) dari sel-sel yang dikunjungi yang memiliki semua pointer mereka dilacak.T
kami juga mencatat himpunan semua sel dalam tumpukan, apakah digunakan atau tidak.H
Hanya dan , atau dan , yang perlu diwakili, agar algoritma bekerja.U U TV U U T
Algoritme dimulai dari beberapa root pointer yang dikenal dengan sistem run-time (biasanya pointer dalam memori yang dialokasikan stack), dan menempatkan semua sel yang mereka tunjuk dalam set tidak dilacak (maka dalam juga).VU V
Kemudian kolektor mengambil sel dalam satu per satu, dan memeriksa setiap sel semua petunjuknya. Untuk setiap pointer, jika sel runcing berada di , maka tidak ada yang dilakukan, selain itu sel runcing ditambahkan ke , karena penunjuknya belum diperiksa. Ketika semua pointer yang telah diproses, sel ditransfer dari set untraced ke set ditelusuri .c V U c U TU c V U c U T
Pelacakan berakhir ketika kosong. Ini pasti terjadi, karena tidak ada sel yang melewati lebih dari sekali. Pada titik itu, , dan semua sel dalam diketahui dapat diakses oleh program, sehingga tidak dapat direklamasi. Komplemen dari di heap menentukan sel-sel apa yang tidak dapat dijangkau oleh program mutator dan dapat direklamasi oleh kolektor untuk alokasi masa depan ke mutator.U V = T V H - V VU U V=T V H−V V
Tentu saja, detailnya bervariasi tergantung pada bagaimana set diimplementasikan, dan apakah itu dan , atau dan , yang diwakili secara efektif.U U TV U U T
Saya juga melewatkan detail tentang apa itu sel, apakah mereka datang dalam satu ukuran atau banyak, bagaimana kita menemukan pointer di dalamnya, bagaimana mereka dapat dipadatkan, dan sejumlah masalah teknis lainnya yang dapat Anda temukan dalam buku dan survei tentang pengumpulan sampah .
Anda mungkin memperhatikan bahwa ini adalah algoritma yang sangat sederhana. Tidak ada rekursi, tetapi hanya satu lingkaran pada elemen-elemen himpunan yang dapat tumbuh saat sedang diprosesU , sampai akhirnya kosong. Tidak ada asumsi apriori tentang memori tambahan.
Apa pun yang memungkinkan mengidentifikasi set, dan melakukan cukup murah operasi yang diperlukan akan dilakukan. Perhatikan bahwa urutan pemrosesan sel tidak relevan (tidak perlu tumpukan pushdown), yang memberikan banyak kebebasan untuk memilih cara untuk mewakili set secara efisien.
Dimana implementasi yang dikenal berbeda dalam cara set ini sebenarnya diwakili. Banyak teknik yang sebenarnya telah digunakan:
bit map: Beberapa ruang memori disimpan untuk peta yang memiliki satu bit untuk setiap sel memori, yang dapat ditemukan menggunakan alamat sel. Bit aktif ketika sel yang sesuai berada di set yang ditentukan oleh peta. Jika hanya peta bit yang digunakan, Anda hanya perlu 2 bit per sel.
sebagai alternatif, Anda mungkin memiliki ruang untuk bit tag khusus (atau 2) di setiap sel untuk menandainya.
Anda dapat menguji predikat pada konten sel, dan petunjuknya.
Anda dapat memindahkan sel di bagian memori bebas yang ditujukan untuk semua sel yang hanya dimiliki oleh perangkat yang diwakili.
Anda sebenarnya dapat menggabungkan teknik-teknik ini, bahkan untuk satu set tunggal.
Seperti yang dikatakan, semua hal di atas telah digunakan oleh beberapa pengumpul sampah yang diimplementasikan, aneh karena beberapa mungkin terlihat. Itu semua tergantung pada berbagai kendala implementasi. Dan mereka bisa agak murah dalam penggunaan memori, mungkin dibantu dengan memproses kebijakan pesanan yang dapat dipilih secara bebas untuk tujuan itu, karena mereka tidak masalah untuk hasil akhirnya.
Apa yang mungkin tampak paling aneh, mentransfer sel di area baru, sebenarnya sangat umum: ini disebut kumpulan salinan. Sebagian besar digunakan dengan memori virtual.
Jelas tidak ada rekursi, dan tumpukan algoritma mutator tidak harus digunakan.
Poin penting lainnya adalah banyak GC modern diterapkan untuk memori virtual besar . Maka mendapatkan ruang untuk mengimplementasikan dan daftar atau tumpukan tambahan tidak menjadi masalah karena halaman baru dapat dengan mudah dialokasikan. Namun, dalam ingatan virtual yang besar, musuh bukanlah kekurangan ruang tetapi kurangnya lokalitas . Kemudian, struktur yang mewakili set, dan penggunaannya, harus diarahkan untuk melestarikan lokalitas struktur data dan pelaksanaan GC. Masalahnya bukan ruang tetapi waktu. Implementasi yang tidak memadai lebih cenderung menunjukkan perlambatan yang tidak dapat diterima daripada limpahan memori.
Saya tidak memberikan referensi ke banyak algoritma spesifik, yang dihasilkan dari berbagai kombinasi teknik ini, karena ini tampaknya cukup lama.
sumber
Cara standar untuk menghindari stack overflow adalah dengan menggunakan stack eksplisit (disimpan sebagai struktur data di heap). Itu juga berfungsi untuk tujuan ini. Pengumpul sampah sering kali memiliki daftar benda kerja yang perlu diperiksa / dilalui, yang melayani peran ini. Misalnya, antrian "Tidak dipindai" Anda adalah contoh pola semacam ini. Antrian berpotensi menjadi besar, tetapi tidak menyebabkan stack overflow, karena tidak disimpan di segmen stack. Bagaimanapun, itu tidak akan pernah menjadi lebih besar dari jumlah objek hidup di heap.
sumber
Dalam deskripsi "klasik" tentang pengumpulan sampah (mis., Mark Wilson, " Teknik Pengumpulan Sampah Uniprocessor ", Lokakarya Int'l tentang Manajemen Memori , 1992, ( tautan alternatif ), atau deskripsi dalam Implementasi Kompiler Modern Andrew Appel (Cambridge University Press, 1998)), kolektor diklasifikasikan sebagai "Tandai dan Sapu" atau "Menyalin".
Tandai dan kolektor Sapu menghindari membutuhkan ruang ekstra dengan menggunakan pointer-reversal, seperti yang dijelaskan dalam jawaban @ Gilles. Appel mengatakan bahwa Knuth mengaitkan algoritma pointer-reversal ke Peter Deutsch, dan ke Herbert Schorr dan WM Waite.
Menyalin pengumpul sampah menggunakan apa yang sering disebut algoritma Cheyney untuk melakukan antrian traversal tanpa memerlukan ruang ekstra. Algoritma ini diperkenalkan di CJ Cheyney, "A Nonrecursive List Compacting Algorithm", Comm. ACM , 13 (11): 677-678, 1970.
Di pengumpul sampah menyalin Anda memiliki sepotong memori yang Anda coba kumpulkan, disebut dari-ruang , dan sepotong memori yang Anda gunakan untuk salinan yang disebut ke-ruang .
scan
To -space diatur sebagai antrian dengan penunjuk yang menunjuk ke catatan terlama yang tidak disalin, danfree
penunjuk yang menunjuk ke posisi bebas berikutnya di ruang. Gambar ini dari makalah Wilson adalah:Saat Anda memindai setiap item di ruang, Anda menyalin anak-anak dari ruang ke
free
pointer di ruang, dan kemudian mengubah pointer ke anak dari dari ruang ke salinan baru anak di ruang. Ada trik tambahan yang perlu Anda gunakan ketika struktur data Anda bukan pohon (ketika seorang anak dapat memiliki lebih dari satu orangtua). Dalam hal itu, ketika Anda menyalin anak dari dari ruang ke ruang, Anda harus menimpa versi lama anak dengan pointer penerusan ke salinan baru anak. Kemudian jika Anda pernah memindai pointer lain ke versi lama anak Anda menyadari bahwa itu sudah disalin, dan jangan salin lagi.sumber