Memahami pengumpulan sampah di .NET

170

Pertimbangkan kode di bawah ini:

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();
    }
}

Sekarang, meskipun variabel c1 dalam metode utama berada di luar ruang lingkup dan tidak direferensikan lebih lanjut oleh objek lain ketika GC.Collect()dipanggil, mengapa tidak diselesaikan di sana?

Victor Mukherjee
sumber
8
GC tidak segera membebaskan instance ketika mereka berada di luar ruang lingkup. Ia melakukannya ketika dianggap perlu. Anda dapat membaca segala sesuatu tentang GC di sini: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061
@ user1908061 (Pssst. Tautan Anda rusak.)
Dragomok

Jawaban:

352

Anda tersandung di sini dan menarik kesimpulan yang sangat salah karena Anda menggunakan debugger. Anda harus menjalankan kode seperti yang dijalankan pada mesin pengguna Anda. Beralihlah ke rilis build terlebih dahulu dengan Build + Configuration manager, ubah combo "Active solution configuration" di sudut kiri atas ke "Release". Selanjutnya, masuk ke Tools + Options, Debugging, General dan hapus centang pada opsi "Suppress JIT optimization".

Sekarang jalankan program Anda lagi dan mainkan kode sumbernya. Perhatikan bagaimana kawat gigi tambahan tidak berpengaruh sama sekali. Dan perhatikan bagaimana pengaturan variabel ke nol tidak ada bedanya sama sekali. Itu akan selalu mencetak "1". Sekarang bekerja seperti yang Anda harapkan dan diharapkan akan berhasil.

Yang tidak pergi dengan tugas menjelaskan mengapa ia bekerja sangat berbeda ketika Anda menjalankan membangun Debug. Itu membutuhkan penjelasan bagaimana pengumpul sampah menemukan variabel lokal dan bagaimana hal itu dipengaruhi oleh kehadiran debugger.

Pertama, jitter melakukan dua tugas penting ketika mengkompilasi IL untuk suatu metode menjadi kode mesin. Yang pertama sangat terlihat di debugger, Anda dapat melihat kode mesin dengan jendela Debug + Windows + Disassembly. Namun tugas kedua sama sekali tidak terlihat. Itu juga menghasilkan tabel yang menggambarkan bagaimana variabel lokal di dalam tubuh metode digunakan. Tabel itu memiliki entri untuk setiap argumen metode dan variabel lokal dengan dua alamat. Alamat tempat variabel pertama-tama akan menyimpan referensi objek. Dan alamat instruksi kode mesin tempat variabel itu tidak lagi digunakan. Juga apakah variabel itu disimpan pada bingkai tumpukan atau register cpu.

Tabel ini sangat penting bagi pengumpul sampah, perlu tahu di mana mencari referensi objek ketika melakukan koleksi. Cukup mudah dilakukan ketika referensi adalah bagian dari objek di heap GC. Jelas tidak mudah dilakukan ketika referensi objek disimpan dalam register CPU. Tabel mengatakan di mana mencarinya.

Alamat "tidak lagi digunakan" dalam tabel sangat penting. Itu membuat pengumpul sampah sangat efisien . Itu dapat mengumpulkan referensi objek, bahkan jika itu digunakan di dalam suatu metode dan metode itu belum selesai dieksekusi. Yang sangat umum, metode Main () Anda misalnya hanya akan pernah berhenti mengeksekusi tepat sebelum program Anda berakhir. Jelas Anda tidak ingin referensi objek apa pun yang digunakan di dalam metode Main () hidup selama durasi program, yang berarti kebocoran. Jitter dapat menggunakan tabel untuk menemukan bahwa variabel lokal seperti itu tidak lagi berguna, tergantung pada sejauh mana program telah berkembang di dalam metode Main () sebelum melakukan panggilan.

Metode hampir ajaib yang terkait dengan tabel itu adalah GC.KeepAlive (). Ini adalah metode yang sangat istimewa, tidak menghasilkan kode sama sekali. Satu-satunya tugas adalah memodifikasi tabel itu. Itu meluasmasa pakai variabel lokal, mencegah referensi yang disimpannya dari mengumpulkan sampah. Satu-satunya waktu Anda perlu menggunakannya adalah untuk menghentikan GC agar tidak terlalu bersemangat mengumpulkan referensi, yang dapat terjadi dalam skenario interop di mana referensi diteruskan ke kode yang tidak dikelola. Pengumpul sampah tidak dapat melihat referensi yang digunakan oleh kode seperti itu karena tidak dikompilasi oleh jitter sehingga tidak memiliki tabel yang mengatakan di mana harus mencari referensi. Melewati objek delegasi ke fungsi yang tidak dikelola seperti EnumWindows () adalah contoh boilerplate saat Anda perlu menggunakan GC.KeepAlive ().

Jadi, seperti yang Anda tahu dari cuplikan sampel setelah menjalankannya di rilis Rilis, variabel lokal dapat dikumpulkan lebih awal, sebelum metode selesai dieksekusi. Bahkan yang lebih kuat, suatu objek dapat dikumpulkan sementara salah satu metodenya berjalan jika metode itu tidak lagi merujuk pada ini . Ada masalah dengan itu, sangat canggung untuk men-debug metode seperti itu. Karena Anda mungkin meletakkan variabel di jendela Watch atau memeriksanya. Dan itu akan hilang saat Anda melakukan debug jika terjadi GC. Itu akan sangat tidak menyenangkan, sehingga jitter sadar akan ada debugger yang terpasang. Itu kemudian memodifikasitabel dan mengubah alamat "terakhir digunakan". Dan mengubahnya dari nilai normal ke alamat instruksi terakhir dalam metode ini. Yang membuat variabel tetap hidup selama metode belum kembali. Yang memungkinkan Anda untuk tetap menontonnya sampai metode kembali.

Ini sekarang juga menjelaskan apa yang Anda lihat sebelumnya dan mengapa Anda mengajukan pertanyaan. Mencetak "0" karena panggilan GC.Collect tidak dapat mengumpulkan referensi. Tabel tersebut mengatakan bahwa variabel sedang digunakan melewati panggilan GC.Collect (), sampai ke akhir metode. Dipaksa untuk mengatakannya dengan memasang debugger dan menjalankan Debug build.

Mengatur variabel ke nol memang memiliki efek sekarang karena GC akan memeriksa variabel dan tidak akan lagi melihat referensi. Tetapi pastikan Anda tidak terjebak dalam perangkap yang banyak programmer C # jatuh, sebenarnya menulis kode itu sia-sia. Tidak ada bedanya apakah pernyataan itu hadir atau tidak ketika Anda menjalankan kode dalam rilis Rilis. Bahkan, pengoptimal jitter akan menghapus pernyataan itu karena tidak memiliki efek apa pun. Jadi pastikan untuk tidak menulis kode seperti itu, walaupun sepertinya ada efeknya.


Satu catatan terakhir tentang topik ini, inilah yang membuat pemrogram bermasalah yang menulis program kecil untuk melakukan sesuatu dengan aplikasi Office. Para debugger biasanya mendapatkannya di Jalan yang Salah, mereka ingin program Office keluar sesuai permintaan. Cara yang tepat untuk melakukannya adalah dengan menelepon GC.Collect (). Tetapi mereka akan menemukan bahwa itu tidak berfungsi ketika mereka men-debug aplikasi mereka, mengarahkan mereka ke tanah yang tidak pernah datang dengan memanggil Marshal.ReleaseComObject (). Manajemen memori manual, jarang berfungsi dengan baik karena mereka akan dengan mudah mengabaikan referensi antarmuka yang tidak terlihat. GC.Collect () sebenarnya berfungsi, hanya saja tidak ketika Anda men-debug aplikasi.

Hans Passant
sumber
1
Lihat juga pertanyaan saya yang dijawab Hans dengan baik untuk saya. stackoverflow.com/questions/15561025/…
Dave Nay
1
@HansPassant Saya baru saja menemukan penjelasan yang luar biasa ini, yang juga menjawab bagian dari pertanyaan saya di sini: stackoverflow.com/questions/30529379/... tentang GC dan sinkronisasi utas. Satu pertanyaan yang masih saya miliki: Saya bertanya-tanya apakah GC benar-benar memadatkan & memperbarui alamat yang digunakan dalam register (disimpan dalam memori saat ditangguhkan), atau hanya dilewati saja? Sebuah proses yang memperbarui register setelah menangguhkan utas (sebelum resume) terasa bagiku sebagai utas keamanan serius yang diblokir oleh OS.
atlaste
Secara tidak langsung, ya. Utas ditangguhkan, GC memperbarui toko dukungan untuk register CPU. Setelah utas melanjutkan, sekarang menggunakan nilai register yang diperbarui.
Hans Passant
1
@HansPassant, saya sangat menghargai jika Anda menambahkan referensi untuk beberapa detail yang tidak jelas dari pengumpul sampah CLR yang Anda jelaskan di sini?
denfromufa
Tampaknya konfigurasi bijaksana, poin penting adalah bahwa "kode Optimalkan" ( <Optimize>true</Optimize>dalam .csproj) diaktifkan. Ini adalah default dalam konfigurasi "Release". Tetapi jika seseorang menggunakan konfigurasi khusus, penting untuk mengetahui bahwa pengaturan ini penting.
Zero3
34

[Hanya ingin menambahkan lebih jauh pada proses Internalisasi Finalisasi]

Jadi, Anda membuat objek dan ketika objek dikumpulkan, Finalizemetode objek harus dipanggil. Tetapi ada lebih banyak finalisasi daripada asumsi yang sangat sederhana ini.

KONSEP SINGKAT ::

  1. Objek TIDAK menerapkan Finalizemetode, di sana Memori segera diperoleh kembali, kecuali tentu saja, mereka tidak dapat dijangkau oleh
    kode aplikasi lagi

  2. Objek menerapkan FinalizeMetode, Konsep / Pelaksanaan Application Roots, Finalization Queue, Freacheable Queuedatang sebelum mereka dapat direklamasi.

  3. Setiap objek dianggap sampah jika TIDAK dapat dicapai oleh Kode Aplikasi

Asumsikan :: Kelas / Objek A, B, D, G, H JANGAN mengimplementasikan FinalizeMetode dan C, E, F, I, J mengimplementasikan FinalizeMetode.

Ketika aplikasi membuat objek baru, operator baru mengalokasikan memori dari heap. Jika tipe objek berisi Finalizemetode, maka pointer ke objek ditempatkan pada antrian finalisasi .

Oleh karena itu pointer ke objek C, E, F, I, J akan ditambahkan ke antrian finalisasi.

The antrian finalisasi adalah struktur data internal dikendalikan oleh pengumpul sampah. Setiap entri dalam antrian menunjuk ke suatu objek yang seharusnya memiliki Finalizemetode yang dipanggil sebelum memori objek dapat direklamasi. Gambar di bawah ini menunjukkan tumpukan yang berisi beberapa objek. Beberapa objek ini dapat dijangkau dari akar aplikasi, dan ada juga yang tidak. Ketika objek C, E, F, I, dan J dibuat, framework .Net mendeteksi bahwa objek-objek ini memiliki Finalizemetode dan pointer ke objek-objek ini ditambahkan ke antrian finalisasi .

masukkan deskripsi gambar di sini

Ketika GC terjadi (Koleksi 1), objek B, E, G, H, I, dan J ditentukan sebagai sampah. Karena A, C, D, F masih dapat dijangkau oleh Kode Aplikasi yang digambarkan melalui panah dari Kotak kuning di atas.

Pengumpul sampah memindai antrian finalisasi mencari pointer ke objek-objek ini. Ketika sebuah pointer ditemukan, pointer tersebut dihapus dari antrian finalisasi dan ditambahkan ke antrian yang dapat diakses ("F-reachable").

The antrian freachable lain adalah struktur data internal dikendalikan oleh pengumpul sampah. Setiap pointer di antrian yang dapat di- freach mengidentifikasi sebuah objek yang siap Finalizedipanggil metode.

Setelah pengumpulan (Koleksi 1), tumpukan yang dikelola terlihat mirip dengan gambar di bawah ini. Penjelasan yang diberikan di bawah ini:
1.) Memori yang ditempati oleh objek B, G, dan H telah direklamasi segera karena objek-objek ini tidak memiliki metode penyelesaian yang perlu dipanggil .

2.) Namun, memori ditempati oleh objek E, I, dan J tidak dapat direklamasi karena merekaFinalize metode belum dipanggil. Memanggil metode Finalisasi dilakukan dengan antrean yang dapat disimpan.

3.) A, C, D, F masih dapat dijangkau oleh Kode Aplikasi yang digambarkan melalui panah dari Kotak kuning di atas, Jadi mereka TIDAK akan dikumpulkan dalam hal apa pun

masukkan deskripsi gambar di sini

Ada utas runtime khusus yang didedikasikan untuk memanggil metode Finalisasi. Ketika antrian yang dapat dibuka kosong (yang biasanya terjadi), utas ini tertidur. Tetapi ketika entri muncul, utas ini bangun, menghapus setiap entri dari antrian, dan memanggil metode Finalisasi masing-masing objek. Pengumpul sampah memadatkan memori yang dapat direklamasi dan utas runtime khusus mengosongkan antrian yang dapat dibuka , menjalankan Finalizemetode setiap objek . Jadi di sini akhirnya adalah ketika metode Finalisasi Anda dijalankan

Kali berikutnya pemulung dipanggil (2nd Collection), ia melihat bahwa objek yang diselesaikan benar-benar sampah, karena akar aplikasi tidak menunjuk ke sana dan antrian yang tidak dapat dihapus tidak lagi menunjuk ke sana (itu juga KOSONG), oleh karena itu memori untuk objek (E, I, J) hanya direklamasi dari Heap. Lihat gambar di bawah ini dan bandingkan dengan gambar di atas

masukkan deskripsi gambar di sini

Hal penting untuk dipahami di sini adalah bahwa dua GC diperlukan untuk mendapatkan kembali memori yang digunakan oleh objek yang membutuhkan finalisasi . Pada kenyataannya, lebih dari dua koleksi taksi bahkan diperlukan karena benda-benda ini dapat dipromosikan ke generasi yang lebih tua

CATATAN:: The antrian freachable dianggap akar seperti variabel global dan statis adalah akar. Oleh karena itu, jika suatu objek berada pada antrian yang dapat diraih, maka objek tersebut dapat dijangkau dan bukan sampah.

Sebagai catatan terakhir, ingatlah bahwa aplikasi debugging adalah satu hal, Pengumpulan Sampah adalah hal lain dan berfungsi secara berbeda. Sejauh ini Anda tidak dapat MERASA pengumpulan sampah hanya dengan men-debug aplikasi, lebih jauh jika Anda ingin menyelidiki Memori, mulailah dari sini.

RC
sumber