Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Pengumpul sampah .NET (GC) membagi objek menjadi objek kecil dan besar. Ketika objek besar, beberapa atributnya menjadi lebih signifikan daripada jika objek kecil. Misalnya, memampatkannya—yaitu, menyalinnya dalam memori di tempat lain di tumpukan—bisa mahal. Karena itu, pengumpul sampah menempatkan benda besar di tumpukan objek besar (LOH). Artikel ini membahas apa yang memenuhi syarat objek sebagai objek besar, bagaimana objek besar dikumpulkan, dan implikasi performa seperti apa yang ditimbulkan oleh objek besar.
Penting
Artikel ini membahas tumpukan objek besar di .NET Framework dan .NET Core yang berjalan pada sistem Windows saja. Ini tidak mencakup LOH yang berjalan pada implementasi .NET pada platform lain.
Bagaimana objek berakhir di LOH
Jika objek lebih besar dari atau sama dengan ukuran 85.000 byte, objek dianggap sebagai objek besar. Jumlah ini ditentukan oleh penyetelan performa. Ketika permintaan alokasi objek mencapai 85.000 byte atau lebih, runtime mengalokasikannya pada heap objek besar.
Untuk memahami apa artinya ini, berguna untuk memeriksa beberapa dasar tentang pengumpul sampah.
Pengumpul sampah adalah pengumpul generasi. Ini memiliki tiga generasi: generasi 0, generasi 1, dan generasi 2. Alasan untuk memiliki tiga generasi adalah bahwa, pada aplikasi yang diatur dengan baik, sebagian besar objek mati pada generasi 0. Misalnya, di aplikasi server, alokasi yang terkait dengan setiap permintaan harus mati setelah permintaan selesai. Permintaan alokasi dalam penerbangan akan membuatnya menjadi gen1 dan mati di sana. Pada dasarnya, gen1 bertindak sebagai buffer antara area objek muda dan area objek berumur panjang.
Objek yang baru dialokasikan membentuk generasi objek baru dan secara implisit merupakan koleksi generasi 0. Namun, jika mereka adalah objek besar, mereka akan ditempatkan pada heap objek besar (LOH), yang kadang disebut sebagai generasi 3. Generasi 3 adalah generasi fisik yang dikumpulkan secara logis sebagai bagian dari generasi 2.
Objek besar milik generasi 2 karena hanya dikumpulkan selama koleksi generasi 2. Ketika sebuah generasi terkumpul, semua generasi yang lebih muda juga terkumpul. Misalnya, ketika GC generasi 1 terjadi, kedua generasi 1 dan 0 dikumpulkan. Dan ketika GC generasi 2 terjadi, seluruh tumpukan dikumpulkan. Untuk alasan ini, GC generasi 2 juga disebut GC penuh. Artikel ini mengacu pada GC generasi 2 alih-alih GC penuh, tetapi istilahnya dapat dipertukarkan.
Generasi memberikan tampilan logis dari tumpukan GC. Secara fisik, objek hidup di segmen tumpukan terkelola. Segmen timbunan terkelola adalah potongan memori yang dicadangkan GC dari OS dengan memanggil fungsi VirtualAlloc atas nama kode terkelola. Ketika CLR dimuat, GC mengalokasikan dua segmen tumpukan awal: satu untuk objek kecil (tumpukan objek kecil, atau SOH), dan satu untuk objek besar (tumpukan objek besar).
Permintaan alokasi kemudian dipenuhi dengan menempatkan objek terkelola pada segmen tumpukan terkelola ini. Jika objek kurang dari 85.000 byte, objek akan dimasukkan ke segmen untuk SOH; jika tidak, objek akan diletakkan pada segmen LOH. Segmen diterapkan (dalam gugus yang lebih kecil) karena semakin banyak objek yang dialokasikan ke dalamnya. Untuk SOH, objek yang bertahan dari GC dipromosikan ke generasi berikutnya. Objek yang bertahan dari koleksi generasi 0 sekarang dianggap sebagai objek generasi 1, dan sebagainya. Namun, objek yang bertahan dari generasi tertua masih dianggap berada di generasi tertua. Dengan kata lain, penyintas dari generasi 2 adalah objek generasi 2; dan penyintas dari LOH adalah objek LOH (yang dikumpulkan dengan gen2).
Kode pengguna hanya dapat mengalokasikan dalam generasi 0 (objek kecil) atau LOH (objek besar). Hanya GC yang dapat "mengalokasikan" objek di generasi 1 (dengan mempromosikan penyintas dari generasi 0) dan generasi 2 (dengan mempromosikan orang yang selamat dari generasi 1).
Ketika pengumpul sampah dipicu, GC melacak objek yang masih aktif dan memampatkannya. Tetapi karena pemadatan memori mahal, GC melakukan pembersihan pada LOH; GC membuat daftar ruang bebas dari objek mati yang dapat digunakan kembali nanti untuk memenuhi permintaan akan alokasi objek besar. Objek mati yang berdekatan diubah menjadi objek tunggal yang bebas.
.NET Core dan .NET Framework (dimulai dengan .NET Framework 4.5.1) menyertakan GCSettings.LargeObjectHeapCompactionMode properti yang memungkinkan pengguna untuk menentukan bahwa LOH harus dikompilasi selama GC pemblokiran penuh berikutnya. Dan di masa depan, .NET dapat memutuskan untuk memampatkan LOH secara otomatis. Ini berarti bahwa, jika Anda mengalokasikan objek besar dan ingin memastikan bahwa objek tersebut tidak bergerak, Anda masih harus menyematkannya.
Gambar 1 menggambarkan skenario di mana GC membentuk generasi 1 setelah generasi pertama 0 GC di mana Obj1
dan Obj3
mati, dan membentuk generasi 2 setelah GC generasi pertama di mana Obj2
dan Obj5
mati. Perhatikan bahwa ini dan gambar berikut hanya untuk tujuan ilustrasi; mereka berisi sangat sedikit objek untuk menunjukkan dengan lebih baik apa yang terjadi pada timbunan. Pada kenyataannya, lebih banyak objek biasanya terlibat dalam GC.
Gambar 1: Generasi 0 dan generasi 1 GC.
Gambar 2 menunjukkan bahwa setelah GC generasi 2 yang melihat bahwa Obj1
dan Obj2
mati, GC membentuk ruang kosong yang berdampingan dari memori yang dulu ditempati oleh Obj1
dan Obj2
, yang kemudian digunakan untuk memenuhi permintaan alokasi untuk Obj4
. Ruang setelah objek terakhir, Obj3
, hingga akhir segmen juga dapat digunakan untuk memenuhi permintaan alokasi.
Gambar 2: Setelah generasi ke-2 GC
Jika tidak ada cukup ruang kosong untuk mengakomodasi permintaan alokasi objek besar, GC pertama-tama mencoba memperoleh lebih banyak segmen dari OS. Jika gagal, itu memicu GC generasi 2 dengan harapan mengosongkan beberapa ruang.
Selama generasi 1 atau generasi 2 GC, pengumpul sampah melepaskan segmen yang tidak memiliki objek hidup di dalamnya, mengembalikannya ke OS dengan memanggil fungsi VirtualFree. Ruang setelah objek aktif terakhir hingga akhir segmen dibebaskan (kecuali pada segmen sementara tempat gen0/gen1 aktif, di mana pengumpul sampah menyimpan beberapa ruang tetap terkomitmen karena aplikasi Anda akan segera melakukan alokasi di sana). Dan ruang kosong tetap berkomitmen meskipun diatur ulang, yang berarti bahwa OS tidak perlu menulis data di dalamnya kembali ke disk.
Karena LOH hanya dikumpulkan selama GC generasi 2, segmen LOH hanya dapat dibebaskan selama GC seperti itu. Gambar 3 menggambarkan skenario di mana pengumpul sampah mengembalikan satu segmen (segmen 2) kembali ke OS dan membebaskan lebih banyak ruang pada segmen-segmen yang tersisa. Jika perlu menggunakan ruang yang telah didekomit di akhir segmen untuk memenuhi permintaan alokasi objek besar, memori akan dikomit lagi. (Untuk penjelasan tentang commit/decommit, lihat dokumentasi untuk VirtualAlloc.)
Gambar 3: LOH setelah GC generasi ke-2
Kapan objek besar dikumpulkan?
Secara umum, GC terjadi di bawah salah satu dari tiga kondisi berikut:
Alokasi melebihi ambang batas generasi 0 atau ambang batas objek besar.
Ambang batas adalah properti generasi. Batas ambang dalam konteks generasi ditetapkan ketika pengumpul sampah mengalokasikan objek ke dalamnya. Ketika ambang batas terlampaui, GC dipicu pada generasi tersebut. Ketika Anda mengalokasikan objek kecil atau besar, Anda menggunakan generasi 0 dan ambang LOH, masing-masing. Ketika pengumpul sampah mengalokasikan untuk generasi 1 dan 2, ia mengkonsumsi ambang batas mereka. Ambang batas ini disetel secara dinamis saat program berjalan.
Ini adalah kasus umum; sebagian besar GC terjadi karena alokasi pada tumpukan terkelola.
Metode GC.Collect dipanggil.
Jika metode tanpa GC.Collect() parameter dipanggil atau kelebihan beban lain diteruskan GC.MaxGeneration sebagai argumen, LOH dikumpulkan bersama dengan sisa tumpukan terkelola.
Sistem berada dalam situasi memori rendah.
Ini terjadi ketika pengumpul sampah menerima pemberitahuan memori tinggi dari OS. Jika kolektor sampah berpikir bahwa melakukan GC generasi 2 akan produktif, maka akan memicu satu.
Implikasi kinerja LOH
Alokasi pada heap objek besar memengaruhi kinerja dengan cara berikut.
Biaya alokasi.
CLR menjamin bahwa memori untuk setiap objek baru yang diberikannya telah dibersihkan. Ini berarti biaya alokasi objek besar didominasi oleh kliring memori (kecuali memicu GC). Jika diperlukan dua siklus untuk membersihkan satu byte, dibutuhkan 170.000 siklus untuk membersihkan objek besar terkecil. Membersihkan memori objek 16-MB pada mesin 2 GHz membutuhkan waktu sekitar 16 ms. Itu biaya yang agak besar.
Biaya penagihan.
Karena LOH dan generasi 2 dikumpulkan secara bersamaan, jika ambang batas salah satunya terlampaui, pengumpulan generasi 2 akan dipicu. Jika koleksi generasi 2 dipicu karena LOH, generasi 2 tidak akan selalu jauh lebih kecil setelah GC. Jika tidak ada banyak data pada generasi 2, hal ini berdampak minimal. Tetapi jika generasi 2 besar, itu dapat menyebabkan masalah performa jika banyak GC generasi 2 dipicu. Jika banyak objek besar dialokasikan secara sementara dan Anda memiliki SOH besar, Anda bisa menghabiskan terlalu banyak waktu untuk melakukan GC. Selain itu, biaya alokasi dapat meningkat tajam jika Anda terus-menerus mengalokasikan dan melepaskan objek yang sangat besar.
Elemen array dengan tipe data referensi.
Objek yang sangat besar pada LOH biasanya berupa array (sangat jarang ada objek instans yang ukurannya sangat besar). Jika elemen array memiliki banyak referensi, hal ini mengakibatkan biaya yang tidak ada jika elemen tersebut tidak memiliki banyak referensi. Jika elemen tidak berisi referensi apa pun, pengumpul sampah tidak perlu melalui array sama sekali. Misalnya, jika Anda menggunakan array untuk menyimpan simpul di pohon biner, salah satu cara untuk mengimplementasikannya adalah dengan merujuk pada simpul kanan dan kiri menggunakan simpul sebenarnya.
class Node { Data d; Node left; Node right; }; Node[] binary_tr = new Node [num_nodes];
Jika
num_nodes
besar, pengumpul sampah harus melalui setidaknya dua referensi per elemen. Pendekatan alternatif adalah menyimpan indeks simpul kanan dan kiri:class Node { Data d; uint left_index; uint right_index; } ;
Alih-alih merujuk data simpul kiri sebagai
left.d
, Anda menyebutnya sebagaibinary_tr[left_index].d
. Dan pengumpul sampah tidak perlu melihat referensi apa pun untuk node kiri dan kanan.
Dari tiga faktor, dua faktor pertama biasanya lebih signifikan daripada yang ketiga. Karena itu, kami sarankan Anda mengalokasikan kumpulan objek besar yang Anda gunakan kembali alih-alih mengalokasikan objek sementara.
Mengumpulkan data performa untuk LOH
Sebelum mengumpulkan data performa untuk area tertentu, Anda seharusnya sudah melakukan hal berikut:
- Ditemukan bukti bahwa Anda perlu memperhatikan wilayah ini.
- Telah menelusuri area lain yang Anda ketahui, tetapi tidak menemukan apa pun yang dapat menjelaskan masalah performa yang Anda amati.
Untuk informasi selengkapnya tentang dasar-dasar memori dan CPU, lihat blog Memahami masalah sebelum Anda mencoba menemukan solusi.
Anda dapat menggunakan alat berikut untuk mengumpulkan data tentang performa LOH:
Penghitung kinerja Memori CLR .NET
Penghitung kinerja Memori CLR .NET biasanya merupakan langkah pertama yang baik dalam menyelidiki masalah performa (meskipun kami sarankan Anda menggunakan peristiwa ETW). Cara umum untuk melihat penghitung kinerja adalah dengan Monitor Performa (perfmon.exe). Pilih Tambahkan (Ctrl + A) untuk menambahkan penghitung menarik untuk proses yang Anda pedulikan. Anda dapat menyimpan data penghitung kinerja ke file log.
Dua penghitung berikut dalam kategori .NET CLR Memory relevan untuk LOH:
# Koleksi Gen 2
Menampilkan berapa kali GC generasi 2 terjadi sejak proses dimulai. Penghitung meningkat pada akhir pengumpulan generasi 2 (juga disebut pengumpulan sampah penuh). Penghitung ini menampilkan nilai terakhir yang diamati.
Ukuran Tumpuk Objek Besar
Menampilkan ukuran saat ini, dalam byte, termasuk ruang kosong, dari LOH. Penghitung ini diperbarui di akhir pengumpulan sampah, bukan di setiap alokasi.
Anda juga dapat mengkueri penghitung kinerja secara terprogram menggunakan PerformanceCounter kelas . Untuk LOH, tentukan ".NET CLR Memory" sebagai CategoryName dan "Ukuran Tumpukan Objek Besar" sebagai CounterName.
PerformanceCounter performanceCounter = new()
{
CategoryName = ".NET CLR Memory",
CounterName = "Large Object Heap size",
InstanceName = "<instance_name>"
};
Console.WriteLine(performanceCounter.NextValue());
Adalah umum untuk mencatat hitungan secara terprogram sebagai bagian dari proses pengujian rutin. Ketika Anda menemukan penghitung dengan nilai yang tidak biasa, gunakan cara lain untuk mendapatkan data yang lebih rinci untuk membantu penyelidikan.
Nota
Kami menyarankan agar Anda menggunakan event ETW, alih-alih menggunakan penghitung kinerja, karena ETW memberikan informasi yang lebih lengkap.
Peristiwa ETW
Pengumpul sampah menyediakan serangkaian peristiwa ETW yang kaya untuk membantu Anda memahami apa yang dilakukan timbunan dan mengapa. Posting blog berikut menunjukkan cara mengumpulkan dan memahami peristiwa GC dengan ETW:
Untuk mengidentifikasi GC generasi 2 yang berlebihan yang disebabkan oleh alokasi LOH sementara, lihat kolom Alasan Pemicu untuk GC. Untuk pengujian sederhana yang hanya mengalokasikan objek besar sementara, Anda dapat mengumpulkan informasi tentang peristiwa ETW dengan perintah PerfView berikut:
perfview /GCCollectOnly /AcceptEULA /nogui collect
Hasilnya adalah sesuatu seperti ini:
Seperti yang Anda lihat, semua GC adalah GC generasi 2, dan semuanya dipicu oleh AllocLarge, yang berarti bahwa mengalokasikan objek besar memicu GC ini. Kita tahu bahwa alokasi ini bersifat sementara karena kolom Loh Survival Rate % mengatakan 1%.
Anda dapat mengumpulkan peristiwa ETW tambahan yang memberi tahu Anda siapa yang mengalokasikan objek besar ini. Baris perintah berikut:
perfview /GCOnly /AcceptEULA /nogui collect
mengumpulkan peristiwa AllocationTick, yang diaktifkan sekitar setiap alokasi senilai 100k. Dengan kata lain, peristiwa diaktifkan setiap kali objek besar dialokasikan. Anda kemudian dapat melihat salah satu tampilan GC Heap Alloc, yang menunjukkan tumpukan panggilan yang mengalokasikan objek besar:
Seperti yang Anda lihat, ini adalah tes yang sangat sederhana yang hanya mengalokasikan objek besar dari metode Main
-nya.
Debugger
Jika yang Anda miliki hanyalah cadangan memori dan Anda perlu melihat objek apa yang sebenarnya ada di LOH, Anda dapat menggunakan ekstensi Debugger SoS yang disediakan oleh .NET.
Nota
Perintah debugging yang disebutkan di bagian ini berlaku untuk debugger Windows.
Berikut ini menunjukkan sampel output dari menganalisis LOH:
0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment begin allocated size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment begin allocated size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT Count TotalSize Class Name
001521d0 66 2081792 Free
7912273c 63 6663696 System.Byte[]
7912254c 4 8008736 System.Object[]
Total 133 objects
Ukuran timbunan LOH adalah (16.754.224 + 16.699.288 + 16.284.504) = 49.738.016 byte. Antara alamat 023e1000 dan 033db630, 8.008.736 byte ditempati oleh array System.Object objek, 6.663.696 byte ditempati oleh array System.Byte objek, dan 2.081.792 byte ditempati oleh ruang kosong.
Terkadang, debugger menunjukkan bahwa ukuran total LOH kurang dari 85.000 byte. Ini terjadi karena runtime itu sendiri menggunakan LOH untuk mengalokasikan beberapa objek yang lebih kecil dari objek besar.
Karena LOH tidak dikompresi, terkadang LOH dianggap sebagai sumber fragmentasi. Fragmentasi berarti:
Fragmentasi timbunan terkelola, yang ditunjukkan oleh jumlah ruang kosong antara objek terkelola. Di SoS,
!dumpheap –type Free
perintah menampilkan jumlah ruang kosong di antara objek terkelola.Fragmentasi ruang alamat memori virtual (VM), yang merupakan memori yang ditandai sebagai
MEM_FREE
. Anda bisa mendapatkannya dengan menggunakan berbagai perintah debugger di windbg.Contoh berikut menunjukkan fragmentasi di ruang VM:
0:000> !address 00000000 : 00000000 - 00010000 Type 00000000 Protect 00000001 PAGE_NOACCESS State 00010000 MEM_FREE Usage RegionUsageFree 00010000 : 00010000 - 00002000 Type 00020000 MEM_PRIVATE Protect 00000004 PAGE_READWRITE State 00001000 MEM_COMMIT Usage RegionUsageEnvironmentBlock 00012000 : 00012000 - 0000e000 Type 00000000 Protect 00000001 PAGE_NOACCESS State 00010000 MEM_FREE Usage RegionUsageFree … [omitted] -------------------- Usage SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Pct(Busy) Usage 701000 ( 7172) : 00.34% 20.69% : RegionUsageIsVAD 7de15000 ( 2062420) : 98.35% 00.00% : RegionUsageFree 1452000 ( 20808) : 00.99% 60.02% : RegionUsageImage 300000 ( 3072) : 00.15% 08.86% : RegionUsageStack 3000 ( 12) : 00.00% 00.03% : RegionUsageTeb 381000 ( 3588) : 00.17% 10.35% : RegionUsageHeap 0 ( 0) : 00.00% 00.00% : RegionUsagePageHeap 1000 ( 4) : 00.00% 00.01% : RegionUsagePeb 1000 ( 4) : 00.00% 00.01% : RegionUsageProcessParametrs 2000 ( 8) : 00.00% 00.02% : RegionUsageEnvironmentBlock Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB) -------------------- Type SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage 7de15000 ( 2062420) : 98.35% : <free> 1452000 ( 20808) : 00.99% : MEM_IMAGE 69f000 ( 6780) : 00.32% : MEM_MAPPED 6ea000 ( 7080) : 00.34% : MEM_PRIVATE -------------------- State SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage 1a58000 ( 26976) : 01.29% : MEM_COMMIT 7de15000 ( 2062420) : 98.35% : MEM_FREE 783000 ( 7692) : 00.37% : MEM_RESERVE Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
Lebih sering terlihat fragmentasi VM yang disebabkan oleh objek besar sementara yang mengharuskan pengumpul sampah (garbage collector) sering memperoleh segmen tumpukan terkelola baru dari OS dan mengembalikan segmen kosong kembali ke OS.
Untuk memverifikasi apakah LOH menyebabkan fragmentasi VM, Anda dapat mengatur titik henti di VirtualAlloc dan VirtualFree untuk melihat siapa yang memanggilnya. Misalnya, untuk melihat siapa yang mencoba mengalokasikan gugus memori virtual yang lebih besar dari 8 MB dari OS, Anda dapat mengatur titik henti seperti ini:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
Perintah ini masuk ke debugger dan menunjukkan tumpukan pemanggilan hanya jika VirtualAlloc dipanggil dengan ukuran alokasi yang lebih besar dari 8 MB (0x800000).
CLR 2.0 menambahkan fitur yang disebut VM Hoarding yang dapat berguna untuk skenario di mana segmen (termasuk pada tumpukan objek besar dan kecil) sering diperoleh dan dirilis. Untuk menentukan VM Hoarding, Anda menentukan bendera startup yang disebut STARTUP_HOARD_GC_VM
melalui API hosting. Alih-alih merilis segmen kosong kembali ke OS, CLR menonaktifkan memori pada segmen ini dan menempatkannya pada daftar siaga. (Perhatikan bahwa CLR tidak melakukan ini untuk segmen yang terlalu besar.) CLR kemudian menggunakan segmen tersebut untuk memenuhi permintaan segmen baru. Lain kali aplikasi Anda membutuhkan segmen baru, CLR menggunakannya dari daftar siaga ini jika dapat menemukan yang cukup besar.
Penyimpanan VM juga berguna untuk aplikasi yang ingin mempertahankan segmen yang telah mereka peroleh, seperti beberapa aplikasi server yang merupakan aplikasi dominan yang berjalan pada sistem, untuk menghindari pengecualian kehabisan memori.
Kami sangat menyarankan Agar Anda menguji aplikasi dengan hati-hati saat menggunakan fitur ini untuk memastikan aplikasi Anda memiliki penggunaan memori yang cukup stabil.