Fragmentasi Heap Objek Besar

97

Aplikasi C # / .NET yang saya kerjakan mengalami kebocoran memori yang lambat. Saya telah menggunakan CDB dengan SOS untuk mencoba menentukan apa yang terjadi tetapi datanya tampaknya tidak masuk akal jadi saya berharap salah satu dari Anda mungkin pernah mengalami ini sebelumnya.

Aplikasi ini berjalan pada kerangka 64 bit. Ini terus menghitung dan menserialisasikan data ke host jarak jauh dan mengenai Large Object Heap (LOH) sedikit lebih baik. Namun, sebagian besar objek LOH yang saya harapkan bersifat sementara: setelah penghitungan selesai dan telah dikirim ke host jarak jauh, memori harus dibebaskan. Apa yang saya lihat, bagaimanapun, adalah sejumlah besar array objek (hidup) yang disisipkan dengan blok memori bebas, misalnya, mengambil segmen acak dari LOH:

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Jelas saya mengharapkan hal ini terjadi jika aplikasi saya membuat objek besar yang berumur panjang selama setiap perhitungan. (Itu melakukan ini dan saya menerima akan ada tingkat fragmentasi LOH tetapi itu bukan masalah di sini.) Masalahnya adalah array objek yang sangat kecil (1056 byte) yang dapat Anda lihat di dump di atas yang tidak dapat saya lihat dalam kode sedang dibuat dan yang entah bagaimana tetap berakar.

Perhatikan juga bahwa CDB tidak melaporkan tipe saat segmen tumpukan dibuang: Saya tidak yakin apakah ini terkait atau tidak. Jika saya membuang objek yang ditandai (<-), CDB / SOS melaporkannya dengan baik:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Elemen-elemen dari larik objek adalah semua string dan string dapat dikenali dari kode aplikasi kita.

Juga, saya tidak dapat menemukan akar GC mereka karena perintah! GCRoot hang dan tidak pernah kembali (saya bahkan mencoba meninggalkannya dalam semalam).

Jadi, saya akan sangat menghargai jika ada yang bisa menjelaskan mengapa array objek kecil (<85k) ini berakhir di LOH: situasi apa yang akan. NET menempatkan array objek kecil di sana? Juga, adakah yang kebetulan mengetahui cara alternatif untuk memastikan akar dari benda-benda ini?


Perbarui 1

Teori lain yang saya kemukakan kemarin adalah bahwa larik objek ini dimulai dengan ukuran besar tetapi telah menyusut meninggalkan blok memori bebas yang terbukti dalam dump memori. Yang membuat saya curiga adalah bahwa array objek selalu tampak sepanjang 1056 byte (128 elemen), 128 * 8 untuk referensi dan 32 byte overhead.

Idenya adalah bahwa mungkin beberapa kode yang tidak aman di perpustakaan atau di CLR merusak bidang jumlah elemen di header array. Agak jauh aku tahu ...


Perbarui 2

Berkat Brian Rasmussen (lihat jawaban yang diterima), masalah telah diidentifikasi sebagai fragmentasi LOH yang disebabkan oleh tabel intern string! Saya menulis aplikasi tes cepat untuk mengonfirmasi ini:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

Aplikasi pertama kali membuat dan membedakan string unik dalam satu lingkaran. Ini hanya untuk membuktikan bahwa memori tidak bocor dalam skenario ini. Jelas seharusnya tidak dan tidak.

Di loop kedua, string unik dibuat dan disimpan. Tindakan ini mengakar mereka di tabel magang. Yang tidak saya sadari adalah bagaimana tabel magang direpresentasikan. Tampaknya itu terdiri dari satu set halaman - array objek dari 128 elemen string - yang dibuat di LOH. Ini lebih jelas di CDB / SOS:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

Mengambil dump segmen LOH mengungkapkan pola yang saya lihat dalam aplikasi yang bocor:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Perhatikan bahwa ukuran array objek adalah 528 (bukan 1056) karena workstation saya 32 bit dan server aplikasi 64 bit. Array objek masih 128 elemen.

Jadi moral dari cerita ini adalah dengan sangat berhati-hati saat magang. Jika string yang Anda interning tidak diketahui sebagai anggota dari himpunan terbatas maka aplikasi Anda akan bocor karena fragmentasi LOH, setidaknya di CLR versi 2.

Dalam kasus aplikasi kita, ada kode umum di jalur kode deserialisation yang memasukkan pengenal entitas selama unmarshalling: Saya sekarang sangat curiga ini pelakunya. Namun, niat pengembang jelas bagus karena mereka ingin memastikan bahwa jika entitas yang sama dideserialisasikan beberapa kali maka hanya satu contoh string pengenal yang akan dipertahankan dalam memori.

Paul Ruane
sumber
2
Pertanyaan bagus - Saya telah memperhatikan hal yang sama di aplikasi saya. Benda-benda kecil tertinggal di LOH setelah balok-balok besar dibersihkan, dan menyebabkan masalah fragmentasi.
Reed Copsey
2
Saya setuju, pertanyaan bagus. Saya akan mencari jawaban.
Charlie Flowers
2
Sangat menarik. Sepertinya cukup bermasalah untuk di-debug!
Matt Jordan

Jawaban:

47

CLR menggunakan LOH untuk mengalokasikan beberapa objek (seperti array yang digunakan untuk string internal ). Beberapa di antaranya kurang dari 85000 byte dan oleh karena itu biasanya tidak akan dialokasikan pada LOH.

Ini adalah detail implementasi, tetapi saya berasumsi alasannya adalah untuk menghindari pengumpulan sampah yang tidak perlu dari contoh yang seharusnya bertahan selama prosesnya sendiri.

Juga karena optimasi yang agak esoterik, salah double[]satu dari 1000 atau lebih elemen juga dialokasikan pada LOH.

Brian Rasmussen
sumber
Objek yang bermasalah adalah objek [] yang berisi referensi untuk string yang saya tahu sedang dibuat oleh kode aplikasi. Ini menyiratkan bahwa aplikasi membuat objek [] (saya tidak dapat melihat buktinya) atau bahwa beberapa bagian dari CLR (seperti serialisasi) menggunakannya untuk mengerjakan objek aplikasi.
Paul Ruane
1
Itu bisa menjadi struktur internal yang digunakan untuk string internal. Silakan periksa jawaban saya untuk pertanyaan ini untuk lebih jelasnya: stackoverflow.com/questions/372547/…
Brian Rasmussen
Ah, ini petunjuk yang sangat menarik, terima kasih. Benar-benar lupa tentang meja magang. Saya tahu salah satu pengembang kami adalah seorang interner yang tajam jadi ini pasti sesuatu yang harus saya selidiki.
Paul Ruane
1
85000 byte atau 84 * 1024 = 87040 byte?
Peter Mortensen
5
85000 byte. Anda dapat memverifikasi ini dengan membuat array byte 85000-12 (ukuran panjang, MT, blok sinkronisasi) dan memanggil GC.GetGenerationinstans. Ini akan mengembalikan Gen2 - API tidak membedakan antara Gen2 dan LOH. Buat array satu byte lebih kecil dan API akan mengembalikan Gen0.
Brian Rasmussen
13

.NET Framework 4.5.1, memiliki kemampuan untuk secara eksplisit memadatkan tumpukan objek besar (LOH) selama pengumpulan sampah.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

Lihat info selengkapnya di GCSettings.LargeObjectHeapCompactionMode

Andre Abrantes
sumber
2

Saat membaca deskripsi tentang cara kerja GC, dan bagian tentang bagaimana objek berumur panjang berakhir di generasi 2, dan pengumpulan objek LOH terjadi hanya pada koleksi lengkap - seperti halnya koleksi generasi 2, gagasan yang muncul di benak adalah. .. mengapa tidak menyimpan generasi 2 dan objek besar di heap yang sama, karena mereka akan dikumpulkan bersama?

Jika itu yang sebenarnya terjadi, maka itu akan menjelaskan bagaimana benda-benda kecil berakhir di tempat yang sama dengan LOH - jika mereka berumur cukup lama untuk berakhir di generasi 2.

Jadi masalah Anda tampaknya merupakan sanggahan yang cukup bagus untuk ide yang muncul pada saya - ini akan mengakibatkan fragmentasi LOH.

Ringkasan: masalah Anda dapat dijelaskan oleh LOH dan generasi 2 yang berbagi wilayah heap yang sama, meskipun itu sama sekali bukan bukti bahwa ini adalah penjelasannya.

Pembaruan: keluaran dari !dumpheap -statcukup banyak pukulan teori ini keluar dari air! Generasi 2 dan LOH memiliki wilayahnya masing-masing.

Daniel Earwicker
sumber
Gunakan! Eeheap untuk menampilkan segmen yang menyusun setiap tumpukan. Gen 0 dan gen 1 hidup dalam satu segmen (segmen yang sama), gen 2 dan LOH keduanya dapat mengalokasikan beberapa segmen tetapi segmen tersebut untuk setiap heap tetap terpisah.
Paul Ruane
Ya, lihat itu, terima kasih. Hanya ingin menyebutkan perintah! Eeheaps karena perintah ini menunjukkan perilaku ini dengan cara yang lebih jelas.
Paul Ruane
Efisiensi GC utama sebagian besar berasal dari fakta bahwa GC dapat merelokasi objek sehingga hanya akan ada sejumlah kecil wilayah memori bebas di heap utama. Jika objek di heap utama disematkan selama pengumpulan, ruang di atas dan di bawah objek yang disematkan mungkin harus dilacak secara terpisah, tetapi karena jumlah objek yang disematkan biasanya sangat kecil, begitu juga dengan jumlah area terpisah yang GC harus jalur. Mencampur objek (besar) yang dapat direlokasi dan yang tidak dapat direlokasi dalam heap yang sama akan mengganggu kinerja.
supercat
Pertanyaan yang lebih menarik adalah mengapa .NET menempatkan doublearray yang lebih besar dari 1000 elemen pada LOH, daripada mengubah GC untuk memastikan bahwa mereka selaras pada batas 8-byte. Sebenarnya, bahkan pada sistem 32-bit saya berharap bahwa karena perilaku cache, memaksakan penyelarasan 8-byte pada semua objek yang dialokasikan ukuran kelipatan 8 byte mungkin akan menjadi kemenangan kinerja. Jika tidak, sementara kinerja yang banyak digunakan double[]yang selaras dengan cache akan lebih baik daripada yang tidak, saya tidak tahu mengapa ukuran akan berkorelasi dengan penggunaan.
supercat
@supercat Selain itu, kedua tumpukan juga berperilaku sangat berbeda dalam alokasi. Heap utama (saat ini) pada dasarnya adalah tumpukan dalam pola alokasi - selalu dialokasikan di bagian atas, mengabaikan ruang kosong apa pun - saat pemadatan terjadi, ruang kosong diperas. Hal ini membuat alokasi hampir tanpa operasi, dan membantu lokalitas data. Di sisi lain, mengalokasikan pada LOH mirip dengan cara kerja malloc - ini akan menemukan tempat gratis pertama yang dapat menampung apa yang Anda alokasikan, dan mengalokasikannya di sana. Karena ini untuk objek besar, lokalitas data diberikan, dan penalti untuk alokasi tidak terlalu buruk.
Luaan
1

Jika format dikenali sebagai aplikasi Anda, mengapa Anda belum mengidentifikasi kode yang menghasilkan format string ini? Jika ada beberapa kemungkinan, coba tambahkan data unik untuk mencari tahu jalur kode mana yang menjadi penyebabnya.

Fakta bahwa array disisipkan dengan item besar yang dibebaskan membuat saya menebak bahwa mereka awalnya berpasangan atau setidaknya terkait. Cobalah untuk mengidentifikasi objek yang dibebaskan untuk mencari tahu apa yang menghasilkannya dan string terkait.

Setelah Anda mengidentifikasi apa yang menghasilkan string ini, cobalah untuk mencari tahu apa yang membuat mereka tidak di-GC. Mungkin mereka dimasukkan ke dalam daftar yang terlupakan atau tidak terpakai untuk tujuan logging atau yang serupa.


EDIT: Abaikan wilayah memori dan ukuran array tertentu untuk saat ini: cari tahu apa yang dilakukan dengan string ini yang menyebabkan kebocoran. Coba! GCRoot ketika program Anda telah membuat atau memanipulasi string ini hanya sekali atau dua kali, saat objek yang harus dilacak lebih sedikit.

HUAGHAGUAH
sumber
String adalah campuran dari Panduan (yang kami gunakan) dan kunci string yang mudah diidentifikasi. Saya dapat melihat di mana mereka dihasilkan, tetapi mereka tidak pernah (secara langsung) ditambahkan ke array objek dan kami tidak secara eksplisit membuat 128 elemen array. Array kecil ini seharusnya tidak berada di LOH untuk memulai.
Paul Ruane
1

Pertanyaan bagus, saya belajar dengan membaca pertanyaan.

Saya pikir bit lain dari jalur kode deserialisation juga menggunakan tumpukan objek besar, maka fragmentasi. Jika semua string diinternir pada waktu yang sama, saya pikir Anda akan baik-baik saja.

Mengingat seberapa baik pengumpul sampah .net, membiarkan jalur kode deserialisation membuat objek string normal kemungkinan sudah cukup baik. Jangan melakukan sesuatu yang lebih rumit sampai kebutuhan itu terbukti.

Saya paling banyak akan melihat tabel hash dari beberapa string terakhir yang telah Anda lihat dan menggunakan kembali ini. Dengan membatasi ukuran tabel hash dan meneruskan ukuran saat Anda membuat tabel, Anda dapat menghentikan sebagian besar fragmentasi. Anda kemudian membutuhkan cara untuk menghapus string yang belum lama ini Anda lihat dari tabel hash untuk membatasi ukurannya. Tetapi jika string yang dibuat oleh jalur kode deserialisation berumur pendek, Anda tidak akan mendapatkan banyak jika ada.

Ian Ringrose
sumber
1

Berikut adalah beberapa cara untuk Mengidentifikasi tumpukan panggilan yang tepat dari alokasi LOH .

Dan untuk menghindari fragmentasi LOH Alokasikan terlebih dahulu berbagai objek dan pin mereka. Gunakan kembali benda-benda ini saat dibutuhkan. Berikut adalah posting tentang Fragmentasi LOH. Sesuatu seperti ini dapat membantu menghindari fragmentasi LOH.

Naveen
sumber
Saya tidak mengerti mengapa memasang pin di sini bisa membantu? Benda-benda besar BTW di LOH tidak digerakkan oleh GC. Ini adalah detail implementasi.
pengguna492238