Sistemlerde büyük nesne Windows yığını
.NET atık toplayıcısı (GC), nesneleri küçük ve büyük nesnelere böler. Bir nesne büyük olduğunda, bazı öznitelikleri nesnenin küçük olmasından daha önemli hale geldi. Örneğin sıkıştırmak, — yığının başka bir yerindeki belleğe kopyalamak — pahalı olabilir. Bu nedenle, atık toplayıcı büyük nesne yığınına (LOH) büyük nesneler yer alır. Bu makalede, bir nesneyi büyük nesne olarak nitelenin ne olduğu, büyük nesnelerin nasıl toplanmış olduğu ve büyük nesnelerin ne tür performans etkilerine neden olduğu açıklanmıştır.
Önemli
Bu makalede, .NET Framework ve .NET Core'daki büyük nesne yığını yalnızca Windows üzerinde çalışır. Diğer platformlarda .NET uygulamaları üzerinde çalışan LOH'ı kapsıyor.
Bir nesnenin LOH'a nasıl var olduğu
Bir nesnenin boyutu 85.000 bayttan büyükse veya ona eşitse, büyük bir nesne olarak kabul edilir. Bu sayı, performans ayarlaması tarafından belirlendi. Bir nesne ayırma isteği 85.000 veya daha fazla bayt için olduğunda, çalışma zamanı bunu büyük nesne yığınına ayırır.
Bunun ne anlama geldiğini anlamak için atık toplayıcıyla ilgili bazı temel bilgiler incelendiğinde yararlı olur.
Atık toplayıcı bir nesil toplayıcıdır. Üç nesli vardır: nesil 0, 1. nesil ve 2. nesil. Üç nesile sahip olmak için iyi ayarlanmış bir uygulamada çoğu nesne gen0'de yok olur. Örneğin, bir sunucu uygulamasında, istek tamam olduktan sonra her istekle ilişkili ayırmalar son bu kadar olur. Uçuş sırasında ayırma istekleri bunu 1. Nesil'e dönüştürecek ve orada son haline gelecektir. Temelde Gen1, genç nesne alanları ile uzun süreli nesne alanları arasında bir arabellek görevi görür.
Yeni ayrılan nesneler yeni bir nesne nesli oluşturur ve örtülü olarak nesil 0 koleksiyonlarıdır. Ancak, bunlar büyük nesnelerse, bazen 3. nesil olarak adlandırılan büyük nesne yığınına (LOH) gider. 3. Nesil, 2. nesil kapsamında mantıksal olarak toplanan fiziksel bir nesildir.
Büyük nesneler yalnızca 2. nesil bir koleksiyon sırasında toplanmış olduğundan nesil 2'ye aittir. Bir nesil toplanıyorsa, tüm genç nesilleri de toplanır. Örneğin, 1. nesil gc olduğunda hem 1. kuşak hem de 0. kuşak toplanır. 2. nesil gc olduğunda yığının tamamı toplanır. Bu nedenle, 2. nesil GC'ye tam GC de denir. Bu makale, tam GC yerine 2. nesil GC'ye başvurur, ancak terimler değiştirilebilir.
Nesiller GC yığınının mantıksal bir görünümünü sağlar. Fiziksel olarak, nesneler yönetilen yığın segmentlerinde bulunur. Yönetilen yığın kesimi, yönetilen kod adına VirtualAlloc işlevini çağırarak GC'nin işletim sisteminden ayırması için bir bellek öbbektir. CLR yüklendiğinde, GC iki ilk yığın segmenti ayırır: biri küçük nesneler (küçük nesne yığını veya SOH) ve biri büyük nesneler (büyük nesne yığını) için.
Daha sonra, bu yönetilen yığın segmentlerine yönetilen nesneler koyarak ayırma istekleri karşılar. Nesne 85.000 bayttan küçükse SOH segmentinde yer alır; aksi takdirde, bir LOH segmentinde yer alır. Parçalara daha fazla nesne ayrılırken parçalar (daha küçük öbekler halinde) işlendi. SOH için, BIR GC'de hayatta olan nesneler yeni nesile yükseltildi. Nesil 0 koleksiyonunda hayatta olan nesneler artık 1. nesil nesneler olarak kabul edilir ve bu şekilde devam eder. Ancak, en eski nesilde hayatta olan nesneler hala en eski nesilde kabul edilir. Başka bir deyişle, nesil 2'den hayatta kalanlar nesil 2 nesneleridir; VE LOH'dan hayatta kalanlar LOH nesneleridir (2. Nesil ile toplanır).
Kullanıcı kodu yalnızca nesil 0'da (küçük nesneler) veya LOH'da (büyük nesneler) ayrılabilir. Yalnızca GC, nesil 1'de (nesil 0'dan hayatta kalanları tanıtarak) ve 2. kuşak (nesil 1'den hayatta kalanları tanıtarak) nesneleri "ayırabilirsiniz".
Bir çöp toplama tetiklendiğinde, GC canlı nesneler aracılığıyla izler ve bunları sıkıştırr. Ancak sıkıştırma pahalı olduğundan GC LOH'yi tarar; daha sonra büyük nesne ayırma isteklerini karşılamak için yeniden kullanılabilir olan, boş bir liste oluşturur. Bitişik olarak bulunan ve boş nesneler tek bir boş nesneye yapılır.
.NET Core ve .NET Framework (.NET Framework 4.5.1'den başlayarak), kullanıcıların LOH'nin bir sonraki tam engelleme GC'de sıkıştırıldıklarını belirtmesini sağlayan özelliğini GCSettings.LargeObjectHeapCompactionMode içerir. Gelecekte .NET, LOH'ı otomatik olarak sıkıştırmaya karar ve ardından. Bu, büyük nesneleri ayırarak taşınmamalarını sağlarsanız yine de sabitleyebilirsiniz.
Şekil 1'de, GC'nin ilk nesil 0 GC'den sonra nesil 1'i, ve 'nin cansız olduğu, 1. nesil GC'den sonra ise 2. neslin bulunduğu Obj1 Obj3 bir Obj2 Obj5 senaryoyu göstermektedir. Bu ve aşağıdaki rakamların yalnızca çizim amaçlı olduğunu unutmayın; yığında ne olduğunu daha iyi göstermek için çok az nesne içerirler. Gerçekte, genellikle bir GC'ye daha fazla nesne dahil olur.

Şekil 1: Nesil 0 ve nesil 1 GC.
Şekil 2'de ve olduğunu gören 2. nesil bir GC'den sonra, GC daha sonra ve tarafından kullanılan ve için ayırma isteğini karşılamak için kullanılan bitişik boş alan Obj1 Obj2 Obj1 Obj2 Obj4 oluşturur. Ayırma isteklerini karşılamak için segmentin sonuna kadar olan son nesneden sonra Obj3 gelen alan da kullanılabilir.

Şekil 2: Nesil 2 GC'den sonra
Büyük nesne ayırma isteklerini karşılamak için yeterli boş alan yoksa, GC ilk olarak işletim sisteminden daha fazla segment elde etmek için çalışır. Bu başarısız olursa, biraz alan serbest bırakarak 2. nesil bir GC tetikler.
Nesil 1 veya 2. nesil GC sırasında atık toplayıcı, üzerinde canlı nesne olmayan kesimleri VirtualFree işlevini çağırarak işletim sistemine geri serbest bıraktır. Segmentin sonuna kadar olan son canlı nesneden sonra boşluk kaldırılır (0.0.1. nesil'in canlı olduğu kısa ömürlü segment dışında, atık toplayıcının bir bölümü işlendi çünkü uygulamanız hemen burada yer atılır). Boş alanlar sıfırlanıyor olsa da işlendi olarak kalır, yani işletim sistemi diske veri yazmak zorunda değildir.
LOH yalnızca 2. nesil GC'ler sırasında toplanmış olduğu için, LOH segmenti yalnızca bu gc sırasında serbest bıraklanabilir. Şekil 3'te atık toplayıcının bir segmenti (segment 2) işletim sistemiyle serbest bırakarak kalan segmentlerde daha fazla alan yayma senaryosu yer almaktadır. Büyük nesne ayırma isteklerini karşılamak için kesim sonundaki atlanmış alanı kullanması gerekirse, belleği yeniden işler. (Commit/decommit'in açıklaması için VirtualAlloc belgelerine bakın.

Şekil 3: Nesil 2 GC'den sonraki LOH
Büyük bir nesne ne zaman toplanır?
Genel olarak, gc aşağıdaki üç koşullardan biri altında gerçekleşir:
Ayırma, nesil 0 veya büyük nesne eşiğini aşıyor.
Eşik, bir neslin özelliğidir. Atık toplayıcı, içine nesne ayırıyorsa nesil için bir eşik ayarlanır. Eşik aşılırsa, bu nesilde bir GC tetiklenir. Küçük veya büyük nesneleri ayırırken sırasıyla nesil 0 ve LOH eşiklerini tüketirsiniz. Atık toplayıcı nesil 1 ve 2'ye ayırırken eşiklerini tüketir. Program çalıştırıldığında bu eşikler dinamik olarak ayarlanmıştır.
Tipik durum bu şekildedir; çoğu GC, yönetilen yığında ayırmalar nedeniyle olur.
yöntemi GC.Collect çağrılır.
Parametresiz yöntem çağrılmışsa veya bağımsız değişken olarak başka bir aşırı yükleme geçiriliyorsa, yönetilen yığının geri kalanıyla birlikte GC.Collect() GC.MaxGeneration LOH toplanır.
Sistem yetersiz bellek durumuna sahip.
Bu durum, atık toplayıcı işletim sisteminden yüksek bellek bildirimi aldığında oluşur. Atık toplayıcı, 2. nesil GC'nin verimli olacağını düşünüyorsa, bir tane tetikler.
LOH performansı üzerindeki etkileri
Büyük nesne yığını üzerindeki ayırmalar performansı aşağıdaki yollarla etkiler.
Ayırma maliyeti.
CLR, verdiği her yeni nesne için belleğin temiz olduğunu garanti eder. Bu, büyük bir nesnenin ayırma maliyetine bellek temizleme (GC tetiklemediği sürece) tarafından baskın olduğu anlamına gelir. Bir baytı temizlemek için iki döngü gerekiyorsa, en küçük büyük nesneyi temizlemek için 170.000 döngü gerekir. 2 GHz makinede 16 MB'lık bir nesnenin belleğini temizlemek yaklaşık 16 ms sürer. Bu oldukça büyük bir maliyettir.
Koleksiyon maliyeti.
LOH ve 2. nesil birlikte toplanmış olduğundan, birinin eşiği aşılırsa 2. nesil bir koleksiyon tetiklenir. LoH nedeniyle nesil 2 koleksiyonu tetiklenirse, 2. nesil GC'den sonra çok daha küçük olmayabilir. 2. nesilde çok fazla veri yoksa bu en az etkiyi içerir. Ancak 2. nesil büyükse, çok sayıda nesil 2 GC tetiklenirse performans sorunlarına neden olabilir. Çok sayıda büyük nesne geçici olarak ayrılırsa ve büyük bir SOH'sı varsa, GC'leri yapmak için çok fazla zaman harcayabilirsiniz. Ayrıca, ayırmaya devam eder ve gerçekten büyük nesneleri serbest bıraksanız ayırma maliyeti gerçekten de uzer.
Başvuru türlerine sahip dizi öğeleri.
LOH üzerinde çok büyük nesneler genellikle dizilerdir (gerçekten büyük bir örnek nesnesine sahip olmak çok nadirdir). Bir dizinin öğeleri başvuru bakımından zenginse, öğeler başvuru bakımından zengin olmayan bir maliyete neden olur. Öğe herhangi bir başvuru içermezse, atık toplayıcının diziden hiç geçerek devam etmelerine gerek yoktur. Örneğin, düğümleri ikili bir ağaçta depolamak için bir dizi kullanırsanız, bunu uygulamanın bir yolu bir düğümün sağ ve sol düğümüne gerçek düğümler tarafından başvurmaktır:
class Node { Data d; Node left; Node right; }; Node[] binary_tr = new Node [num_nodes];num_nodesBüyükse, atık toplayıcının öğe başına en az iki başvurudan geçerek devam etmek gerekir. Alternatif bir yaklaşım ise sağ ve sol düğümlerin dizinini depolamaktır:class Node { Data d; uint left_index; uint right_index; } ;Sol düğümün verilerine olarak başvurmak yerine
left.dolarakbinary_tr[left_index].dbaşvurabilirsiniz. Ayrıca atık toplayıcının sol ve sağ düğüm için herhangi bir başvuruya bakması da gerek değildir.
Üç faktörün dışında, ilk ikisi genellikle üçüncüden daha önemlidir. Bu nedenle, geçici nesneleri ayırmak yerine yeniden kullanılan büyük nesnelerden bir havuz ayırmayı öneririz.
LOH için performans verileri toplama
Belirli bir alan için performans verilerini toplamadan önce, şunları zaten yapmış olması gerekir:
Bu alana bakmanız gerektiğine dair kanıt bulundu.
Daha önce gördüğümüz performans sorununu açıklayacak bir şey bulamadan, sahip olduğunuz diğer alanlar tükendi.
Bellek ve CPU ile ilgili temel bilgiler hakkında daha fazla bilgi için, bir çözüm bulmaya çalışmadan önce sorunu anlama bloguna bakın.
LOH performansıyla ilgili veri toplamak için aşağıdaki araçları kullanabilirsiniz:
.NET CLR Bellek Performansı sayaçları
Bu performans sayaçları genellikle performans sorunlarını araştırmanın iyi bir ilk adımıdır (ANCAK ETW olaylarını kullanmanız önerilir). Şekil 4 Performans İzleyicisi de olduğu gibi istediğiniz sayaçları ekleyerek yapılandırmayı yapılandırmış oluruz. LOH ile ilgili olanlar:
2. Nesil Koleksiyonlar
İşlem başlatıldından sonra 2. nesil GC'lerin kaç kez meydana geldiği görüntülenir. Sayaç, nesil 2 toplamanın sonunda (tam çöp toplama olarak da adlandırılan) artırılır. Bu sayaç, gözlemlenen son değeri görüntüler.
Büyük Nesne Yığın boyutu
LOH'nin boş alanı da dahil olmak üzere geçerli boyutu bayt cinsinden görüntüler. Bu sayaç, her ayırmada değil, çöp toplamanın sonunda güncelleştirilir.
Performans sayaçlarını değerlendirmenin yaygın bir yolu, Performans İzleyicisi (perfmon.exe). "Sayaç Ekle" ifadesini kullanarak, önemle önemle önem istediğiniz işlemler için ilgi çekici sayacı ekleyin. Şekil 4'te de olduğu gibi performans sayacı verilerini bir günlük dosyasına kaydedebilirsiniz:
Şekil 4: Nesil 2 GC'den sonraki LOH
Performans sayaçları program aracılığıyla da sorgulandırabilirsiniz. Birçok kişi bunları rutin test sürecinin bir parçası olarak bu şekilde toplar. Sıradan değerlere sahip sayaçları tespit etmek için araştırmada yardımcı olmak üzere daha ayrıntılı veriler elde etmek için başka bir kaynak kullanırlar.
Not
ETW çok daha zengin bilgiler sağladığından, performans sayaçları yerine ETW olaylarını kullanmanız önerilir.
ETW olayları
Atık toplayıcı, yığının ne yaptığını ve neden yaptığını anlamanıza yardımcı olmak için zengin bir ETW olayları kümesi sağlar. Aşağıdaki blog gönderileri, ETW ile GC olaylarını toplamayı ve anlamayı gösterir:
Geçici LOH ayırmalarının neden olduğu aşırı 2. nesil GC'leri belirlemek için, GC'ler için Tetikleyici Nedeni sütununa bakın. Yalnızca geçici büyük nesneleri ayıran basit bir test için aşağıdaki PerfView komut satırıyla ETW olayları hakkında bilgi toplayabilirsiniz:
perfview /GCCollectOnly /AcceptEULA /nogui collect
Sonuç aşağıdakine benzer:
Şekil 5: PerfView kullanılarak gösterilen ETW olayları
Gördüğünüz gibi, tüm GC'ler nesil 2 GC'ler ve bunların hepsi AllocLarge tarafından tetiklenir. Bu da bu GC'yi tetikleyen büyük bir nesne olduğu anlamına gelir. LOH Hayatta Kalma Oranı % sütununda %1 olduğu için bu ayırmaların geçici olduğunu biliyoruz.
Bu büyük nesneleri kimin ayıran olduğunu size haber veren ek ETW olayları toplayabilirsiniz. Aşağıdaki komut satırı:
perfview /GCOnly /AcceptEULA /nogui collect
yaklaşık olarak her 100.000 değerinde ayırmanın ardından bir AllocationTick olayı toplar. Başka bir deyişle, büyük bir nesne her ayrılırken bir olay başlatıldı. Daha sonra, size büyük nesneler ayrılan çağrı yığınlarını göstermek için GC Yığını Ayırma görünümlerinden birini göz atabilirsiniz:
Şekil 6: GC Yığını Ayırma görünümü
Gördüğünüz gibi bu, yönteminden yalnızca büyük nesneleri ayıran çok basit bir Main testtir.
Hata ayıklayıcısı
Tek sahip olduğunuz bellek dökümü ise ve LOH'da gerçekte hangi nesnelerin olduğunu bakmanız gerekirse. .NET tarafından sağlanan SoS hata ayıklayıcısı uzantısını kullanabilirsiniz.
Not
Bu bölümde belirtilen hata ayıklama komutları, hata ayıklayıcıları Windows geçerlidir.
Aşağıda, LOH analizinin örnek çıktısı ve ardından şu şekildedir:
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
LOH yığın boyutu (16.754.224 + 16.699.288 + 16.284.504) = 49.738.016 bayttır. 023e1000 ile 033db630 arasındaki adreslerde bir nesne dizisi tarafından 8.008.736 bayt, bir nesne dizisi tarafından System.Object 6.663.696 bayt ve System.Byte 2.081.792 bayt boş alan tarafından kullanılır.
Bazen hata ayıklayıcısı LOH'nin toplam boyutunun 85.000 bayttan az olduğunu gösterir. Bunun nedeni çalışma zamanının büyük bir nesneden küçük olan bazı nesneleri ayırmak için LOH'ı kullanmasıdır.
LOH sıkıştırılamamış olduğundan, bazen PARÇALANMANIN kaynağı LOH olduğu san gelir. Parçalanma şu anlama gelir:
Yönetilen nesneler arasındaki boş alan miktarıyla belirtilen yönetilen yığının parçalanması. SoS'de,
!dumpheap –type Freekomutu yönetilen nesneler arasındaki boş alan miktarını görüntüler.olarak işaretlenmiş bellek olan sanal bellek (VM) adres alanı
MEM_FREEparçalanması. Bunu almak için windbg'de çeşitli hata ayıklayıcı komutlarını kullanabilirsiniz.Aşağıdaki örnek, VM alanı içinde parçalanma gösterir:
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)
Atık toplayıcının sık sık işletim sisteminden yeni yönetilen yığın kesimleri edinmelerini ve boş olanları işletim sistemine geri bırakmalarını gerektiren geçici büyük nesnelerin neden olduğu VM parçalanması daha yaygın bir durumdur.
LOH'nin VM parçalanmasına neden olup olmadığını doğrulamak için VirtualAlloc ve VirtualFree üzerinde bir kesme noktası ayarerek onları kimin çağırmış olduğunu kontrol etmek için kullanabilirsiniz. Örneğin, işletim sisteminden 8 MB'tan büyük sanal bellek öbeklerini ayırmaya çalışanları görmek için aşağıdakine benzer bir kesme noktası ayarlayın:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
Bu komut hata ayıklayıcıya kırılır ve çağrı yığınını yalnızca 8 MB'den büyük bir ayırma boyutuyla VirtualAlloc çağrılmışsa 0x800000.
CLR 2.0, kesimlerin (büyük ve küçük nesne yığınlarında dahil) sık sık alınarak serbest bırakıldıkları senaryolar için kullanışlı olan VM Hoarding adlı bir özellik ekledi. VM Hoarding belirtmek için barındırma API'si aracılığıyla adlı STARTUP_HOARD_GC_VM bir başlangıç bayrağı belirtirsiniz. CLR, boş segmentleri işletim sistemine geri serbest bırakmak yerine bu segmentlerde belleği alır ve bekleme listesine koyar. (CLR'nin bunu çok büyük olan segmentler için olmadığını unutmayın.) CLR daha sonra yeni segment isteklerini karşılamak için bu kesimleri kullanır. Uygulamanıza yeni bir segment gerektirecek bir sonraki sefer CLR, yeterince büyük bir kesim bulursa bu bekleme listesinden bir kesim kullanır.
YETERSIZ bellek özel durumlarından kaçınmak için, VM'yi daha önce edinilen segmentlerde tutmak isteyen uygulamalar için de yararlıdır. Örneğin, sistemde çalışan baskın uygulamalar olan bazı sunucu uygulamaları.
Bu özelliği kullanarak uygulamanın oldukça kararlı bellek kullanımına sahip olduğundan emin olmak için, uygulamalarınızı dikkatle test edin.