21-Bob: Managed Heap va Garbage Collection

Managed heap asoslari, avlodlar (generations), garbage collection algoritmi, GC rejimlari, finalization, Dispose pattern, weak references va obyekt umrini boshqarish

Ushbu bobda men managed ilovalar qanday qilib yangi obyektlar yaratishini, managed heap obyektlarning umrini qanday boshqarishini va bu obyektlar uchun xotira qanday qaytarib olinishini muhokama qilaman. Qisqasi, men common language runtime (CLR) dagi garbage collector (axlat yig'uvchi) qanday ishlashini tushuntiraman va unga bog'liq turli ishlash masalalarini ko'rib chiqaman. Shuningdek, ilovalarni xotiradan eng samarali foydalanishi uchun qanday loyihalash kerakligini ham muhokama qilaman.

Bobdagi bo'limlar:

  • Managed Heap Asoslari — sahifa 505
  • Avlodlar: Ishlashni Yaxshilash — sahifa 513
  • Maxsus Tozalash Kerak Turlar Bilan Ishlash — sahifa 525
  • Obyektlar Umrini Monitoring va Qo'lda Boshqarish — sahifa 545

Managed Heap Asoslari

Har bir dastur turli resurslardan foydalanadi — fayllar, xotira bufferlari, ekran maydoni, tarmoq ulanishlari, ma'lumotlar bazasi resurslari va hokazo. Aslida, obyektga yo'naltirilgan muhitda har bir tur dastur uchun foydalanish mumkin bo'lgan biror resursni belgilaydi. Ushbu resurslardan foydalanish uchun quyidagi bosqichlar zarur:

  1. Xotira ajratish — resursni ifodalovchi tur uchun xotira ajratish (odatda C# ning new operatori bilan amalga oshiriladi).
  2. Xotirani initsializatsiya qilish — resursning dastlabki holatini o'rnatish va uni foydalanishga yaroqli qilish. Turning instansiya konstruktori bu dastlabki holatni o'rnatish uchun javobgardir.
  3. Resursdan foydalanish — tur a'zolariga murojaat qilib resursdan foydalanish (zarur bo'lganda takrorlash).
  4. Resursni tozalash — resurs holatini yo'qotish.
  5. Xotirani bo'shatish — bu qadam uchun faqat garbage collector javobgar.

Bu ko'rinishda oddiy paradigma xotirani qo'lda boshqarishi kerak bo'lgan dasturchilar uchun muammolarning asosiy manbai bo'lib kelgan. Masalan, native C++ dasturchilari o'z xotirasini boshqarishi kerak. Dasturchilar xotirani bo'shatishni unutib qo'yishadi, bu esa xotira oqishiga (memory leak) olib keladi. Bundan tashqari, bu dasturchilar xotirani bo'shatgandan keyin ham tez-tez ishlatishadi, bu esa xotira buzilishi va xavfsizlik teshiklariga olib keladi.

Siz tekshiriladigan tur-xavfsiz kod yozayotganingizcha (C# ning unsafe kalit so'zidan qochib), ilovangizda xotira buzilishi mumkin emas. Ilovangizda xotira oqishi hali ham mumkin, lekin bu standart xatti-harakat emas. Xotira oqishi odatda ilovangiz obyektlarni kolleksiyaga saqlayotgani va ular endi kerak bo'lmaganda hech qachon olib tashlamasligi sababli yuz beradi.

Ishlarni yanada soddalashtirib, ko'plab turlar muntazam foydalaniladigan turlar bo'lib, 4-bosqichni (resursni tozalash) talab qilmaydi. Shunday qilib, managed heap yuqorida aytib o'tilgan xatolarni yo'q qilishdan tashqari, dasturchilarga oddiy dasturlash modeli bilan ta'minlaydi: ajrating va resurdan foydalaning. Ko'pchilik turlar uchun resursni tozalash yoki xotirani bo'shatish kerak emas — garbage collector xotirani o'zi bo'shatadi.

Managed Heap dan Resurslarni Ajratish

CLR barcha obyektlarni managed heap dan ajratishni talab qiladi. Jarayon initsializatsiya qilinganda, CLR managed heap uchun manzil maydoni mintaqasini ajratadi. CLR shuningdek NextObjPtr deb nomlangan ko'rsatkichni saqlaydi. Bu ko'rsatkich keyingi obyektni heap ichida qaerga joylashtirishni ko'rsatadi. Dastlab, NextObjPtr manzil maydoni mintaqasining boshlang'ich manziliga o'rnatiladi.

Mintaqa axlat bo'lmagan obyektlar bilan to'ldirilganda, CLR ko'proq mintaqalar ajratadi va jarayonning butun manzil maydonini to'ldirguncha davom etadi. Shunday qilib, ilovangiz jarayonning virtual manzil maydoni bilan cheklangan. 32-bitli jarayonda taxminan 1.5 gigabayt (GB), 64-bitli jarayonda esa taxminan 8 terabaytgacha ajratishingiz mumkin.

C# ning new operatori CLR ga quyidagi bosqichlarni bajaradi:

  1. Baytlarni hisoblash — turning maydonlari (va barcha asosiy turlardan meros olingan maydonlar) uchun zarur baytlar sonini hisoblash.
  2. Overhead qo'shish — obyekt overhead uchun kerakli baytlarni qo'shish. Har bir obyektda ikkita overhead maydon bor: tur obyekt ko'rsatkichi va sinxronizatsiya blok indeksi. 32-bitli ilovada har bir maydon 32 bit, har bir obyektga 8 bayt qo'shiladi. 64-bitli ilovada har biri 64 bit, 16 bayt qo'shiladi.
  3. Obyektni joylashtirish — CLR kerakli baytlarni ajratish uchun mintaqada yetarli bo'sh joy borligini tekshiradi. Agar bo'sh joy bo'lsa, obyekt NextObjPtr ko'rsatadigan manzildan boshlab joylashtiriladi va bu baytlar nolga o'rnatiladi. Turning konstruktori chaqiriladi (NextObjPtr this parametr sifatida uzatiladi) va new operatori obyektga havolani qaytaradi. Havola qaytarilishdan oldin, NextObjPtr obyektdan keyingi manzilga ko'chiriladi.
Managed heap ning afzalligi

Managed heap uchun obyektni ajratish oddiy — ko'rsatkichga qiymat qo'shish. Ko'pgina ilovalarda bir vaqtda ajratilgan obyektlar kuchli aloqaga ega va tez-tez bir vaqtda ishlatiladi. Managed heap bu obyektlarni xotirada yonma-yon joylashtirganligi sababli, siz ularni ishlatishda ma'lumotlar joylashuvining (locality of reference) afzalliklaridan foydalanasiz. Jarayoningizning ishchi to'plami (working set) kichik, ilova kamroq xotira bilan tez ishlaydi. CPU keshi ob'ektlarni o'z ichiga olishi mumkin va CPU RAMga murojaat qilmasdan ko'p amallarni bajara oladi.

Hozircha managed heap ajoyib ishlash xususiyatlarini taqdim qilgandek ko'rinadi. Biroq, xotira cheksiz emas. CLR garbage collection (GC) deb nomlanuvchi texnikani qo'llaydi — ilovangiz endi murojaat qilmaydigan heapdagi obyektlarni "o'chirish" uchun.

Garbage Collection Algoritmi

Ilova obyekt yaratish uchun new operatorini chaqirganda, mintaqada obyektni ajratish uchun yetarli manzil maydoni qolmagan bo'lishi mumkin. Agar yetarli joy bo'lmasa, CLR GC bajaradi.

Muhim

Men hozirgina aytganlarim soddalashtirish. Aslida, GC 0-avlod o'z byudjetini to'ldirganda yuz beradi. Men avlodlarni keyinroq tushuntiraman. Hozircha heap to'lganda garbage collection yuz beradi deb o'ylash osonroq.

Obyektlarning umrini boshqarish uchun ba'zi tizimlar havolalarni hisoblash (reference counting) algoritmidan foydalanadi. Microsoftning o'zining Component Object Model (COM) havolalarni hisoblashdan foydalanadi. Havolalarni hisoblash tizimida heapdagi har bir obyekt ichki maydon saqlaydi — dasturning qancha "qismi" hozirda bu obyektdan foydalanayotganligini ko'rsatuvchi. Hisoblash 0 ga yetganda, obyekt o'zini xotiradan o'chiradi. Biroq bu tizimlarning katta muammosi shundaki, ular halqasimon havolalarni (circular references) yaxshi boshqara olmaydi.

Shu sababli CLR havolalarni kuzatish algoritmi (reference tracking algorithm) dan foydalanadi. Bu algoritm faqat reference tur o'zgaruvchilarini ko'rib chiqadi, chunki faqat bu o'zgaruvchilar heapdagi obyektga havola qilishi mumkin. Biz barcha reference tur o'zgaruvchilarini ildizlar (roots) deb ataymiz.

CLR GC boshlaganda, avval jarayondagi barcha oqimlarni to'xtatadi (bu oqimlarning obyektlarga murojaat qilishini va holatlarini o'zgartirishini oldini oladi). Keyin CLR belgilash fazasi (marking phase) ni bajaradi:

  1. Heapdagi barcha obyektlarni ko'rib chiqadi va sinxronizatsiya blok indeksidagi bitni 0 ga o'rnatadi (barcha obyektlar o'chirilishi kerak).
  2. Barcha faol ildizlarni ko'rib chiqadi. Agar ildiz null bo'lsa, uni o'tkazib yuboradi.
  3. Heapdagi obyektga ishora qiluvchi har bir ildiz uchun CLR bu obyektni belgilaydi (sinxronizatsiya blok indeksidagi bitni 1 ga o'rnatadi).
  4. Belgilangan obyektning ichidagi havolalarni ham tekshiradi va ular ko'rsatadigan obyektlarni ham belgilaydi. Agar obyekt allaqachon belgilangan bo'lsa, CLR uning maydonlarini qayta tekshirmaydi — bu halqasimon havolalar uchun cheksiz tsiklni oldini oladi.

Belgilash tugagach, heap ba'zi belgilangan va ba'zi belgilanmagan obyektlarni o'z ichiga oladi. Belgilangan obyektlar kolleksiyadan omon qolishi kerak, chunki kamida bitta ildiz ularga ishora qiladi — biz bu obyektni erishiladigan (reachable) deb ataymiz. Belgilanmagan obyektlar esa erishib bo'lmaydigan (unreachable) — ularga endi hech qanday ildiz yo'q.

Endi CLR siqishtirish fazasi (compacting phase) ni boshlaydi. CLR belgilangan obyektlarni pastga siljitadi, barcha omon qolgan obyektlarni bir-biriga yaqin qilib siqishtiradi. Bu ko'p afzalliklarga ega:

  • Barcha omon qolgan obyektlar bir-birining yonida bo'ladi — bu ma'lumotlar joylashuvini tiklaydi
  • Bo'sh joy ham tutash bo'ladi, shuning uchun bu manzil maydoni mintaqasini boshqa maqsadlarda ishlatish uchun bo'shatish mumkin
  • Siqishtirish managed heap bilan manzil maydoni fragmentatsiya muammolari bo'lmasligini ta'minlaydi

Xotirani siqishtirganda, CLR xotirada obyektlarni ko'chirmoqda. Bu muammo, chunki omon qolgan obyektga ishora qilgan har qanday ildiz endi obyekt xotirada qaerda ekanligini emas, avval qaerda bo'lganligini ko'rsatadi. Shuning uchun CLR siqishtirish fazasida har bir ildizdan obyekt siljitilgan baytlar sonini ayiradi. Bu har bir ildiz avvalgidek bir xil obyektga ishora qilishini ta'minlaydi.

Heap xotirasi siqishtirilgandan keyin, NextObjPtr ko'rsatkichi oxirgi omon qolgan obyektdan keyingi joyga o'rnatiladi. Keyin CLR barcha oqimlarni qayta tiklaydi va ular obyektlarga xuddi hech narsa bo'lmagandek murojaat qilishda davom etadi.

Muhim

Statik maydon u ishora qilgan obyektni abadiy yoki turlar yuklangan AppDomain tushirilguncha tirik tutadi. Xotira oqishining keng tarqalgan usuli — statik maydon kolleksiya obyektiga ishora qilsin va unga elementlar qo'shishda davom etish. Statik maydon kolleksiya obyektini tirik tutadi va kolleksiya barcha elementlarini tirik tutadi. Shu sababli, iloji boricha statik maydonlardan qochish yaxshiroqdir.

GC: Mark-Sweep-Compact Bosqichlari
Boshlang'ich:
A
B
C
D
E
F
G
Mark:
A
B
C
D
E
F
G
Sweep:
A
B
C
D
E
F
G
Compact:
A
B
D
G
1) Tirik obyektlar belgilanadi (Mark) 2) O'lik obyektlar tozalanadi (Sweep) 3) Tiriklar zich joylashtiriladi (Compact)
GC Ishlash Jarayoni (Jonli Animatsiya)
A
B
C
D
E
F
G
H
Yashil = tirik obyekt, Qizil = o'lik obyekt. Kuzating: GC avval belgilaydi (sariq), keyin o'liklarni yo'qotadi.

Garbage Collection va Debugging

Ildiz ko'rinish doirasidan chiqishi bilanoq, u ishora qilgan obyekt erishib bo'lmaydigan bo'ladi va GC tomonidan xotirasi qaytarib olinishi mumkin. Bu ilovangizga qiziqarli ta'sir ko'rsatishi mumkin. Quyidagi kodni ko'rib chiqing:

using System;
using System.Threading;

public static class Program {
    public static void Main() {
        // Timer obyekti yaratish
        Timer t = new Timer(TimerCallback, null, 0, 2000);

        // Foydalanuvchi Enter bosishini kutish
        Console.ReadLine();
    }

    private static void TimerCallback(Object o) {
        // Sana/vaqtni ko'rsatish
        Console.WriteLine("In TimerCallback: " + DateTime.Now);

        // Demo uchun garbage collection ni majburlash
        GC.Collect();
    }
}

Bu kodni hech qanday maxsus kompilyator kalitlarisiz kompilyatsiya qiling va ishga tushiring — TimerCallback metodi faqat bir marta chaqirilganini ko'rasiz!

Sababini tushunaylik: Timer obyekti yaratiladi va t o'zgaruvchisi unga ishora qiladi. Lekin Main metodi dastlabki tayinlashdan keyin t o'zgaruvchisini ishlatmaydi. Shuning uchun TimerCallback ichida GC.Collect() chaqirilganda, GC Timer obyektiga hech qanday o'zgaruvchi murojaat qilmayotganini ko'radi va uni yig'ib oladi — timer to'xtaydi va callback faqat bir marta chaqiriladi.

C# kompilyatorining /debug kaliti bilan kompilyatsiya qilsangiz, kompilyator System.Diagnostics.DebuggableAttribute ni DebuggingModes.DisableOptimizations bayrog'i bilan assembly ga qo'yadi. Ish vaqtida JIT kompilyatori bu bayroqni ko'rib, barcha ildizlarning umrini metod oxirigacha sun'iy ravishda uzaytiradi. Shunda Timer obyekti Main tugaguncha tirik qoladi.

Muammoni to'g'ri hal qilish uchun Timer ni ildiz sifatida saqlash kerak:

public static void Main() {
    Timer t = new Timer(TimerCallback, null, 0, 2000);

    Console.ReadLine();

    // t ni ReadLine dan keyin ham tirik tutish
    // (t Dispose uchun this argument sifatida kerak)
    t.Dispose();
}
Eslatma

Bu muhokamani o'qib, o'z obyektlaringiz vaqtidan oldin garbage collect qilinishi haqida tashvishlanmang. Men bu yerda Timer klassini ishlataman, chunki u maxsus xatti-harakatga ega: Timer obyektining heapda mavjudligi boshqa narsani (thread pool oqimi metodini chaqirishni) keltirib chiqaradi. Barcha Timer bo'lmagan obyektlar ilova tomonidan kerak bo'lganda avtomatik ravishda tirik qoladi.

Avlodlar: Ishlashni Yaxshilash

CLR ning GC si avlodli garbage collector (generational garbage collector) hisoblanadi (shuningdek efemer garbage collector deb ham ataladi). Avlodli GC kodingiz haqida quyidagi taxminlarni qiladi:

  • Obyekt qanchalik yangi bo'lsa, uning umri shunchalik qisqa bo'ladi.
  • Obyekt qanchalik eski bo'lsa, uning umri shunchalik uzoq bo'ladi.
  • Heapning bir qismini yig'ish butun heapni yig'ishdan tezroq.

Ko'plab tadqiqotlar bu taxminlarning mavjud ilovalarning juda katta to'plami uchun haqiqiyligini isbotlagan va bu taxminlar garbage collector qanday amalga oshirilishiga ta'sir qilgan.

Avlodlar Qanday Ishlaydi

Initsializatsiya qilinganda, managed heap hech qanday obyektni o'z ichiga olmaydi. Heapga qo'shilgan obyektlar 0-avlod (generation 0) da deyiladi. Oddiy qilib aytganda, 0-avlod obyektlari — garbage collector hech qachon tekshirmagan yangi yaratilgan obyektlardir.

CLR initsializatsiya qilinganda, 0-avlod uchun byudjet hajmi (kilobaytlarda) tanlaydi. Agar yangi obyektni ajratish 0-avlod byudjetini oshirsa, garbage collection boshlanishi kerak.

Aytaylik, A dan E gacha obyektlar 0-avlodni to'ldiradi. F obyektini ajratishda garbage collection boshlanishi kerak. GC C va E obyektlarini axlat deb aniqlaydi va D ni siqishtiradi (B ga yaqin). Kolleksiyadan omon qolgan A, B va D obyektlari endi 1-avlod (generation 1) da deyiladi — ular garbage collector tomonidan bir marta tekshirilgan.

Garbage collection dan keyin 0-avlod bo'sh bo'ladi va yangi obyektlar shu yerga joylashtiriladi. Ilova ishlashda davom etib, F dan K gacha obyektlarni ajratadi. Bu orada B, H va J erishib bo'lmaydigan bo'lib qoladi.

Endi 0-avlod byudjeti yana oshganda, GC faqat 0-avlodni yig'adi — 1-avlod obyektlarini e'tiborsiz qoldiradi. Chunki avlodli GC ning birinchi taxmini: yangi yaratilgan obyektlarning umri qisqa. Shuning uchun 0-avlodda ko'p axlat bo'lishi ehtimoli yuqori va ko'p xotira qaytarib olinadi. 1-avlod obyektlarini e'tiborsiz qoldirish garbage collection jarayonini tezlashtiradi.

Ishlash

Microsoft ning testlari shuni ko'rsatadiki, 0-avlod garbage collection 1 millisekunddan kam vaqt oladi. Microsoft ning maqsadi — garbage collectionlar oddiy sahifa xatosidan (page fault) ko'p vaqt olmasligi.

0-avlod omon qolganlari 1-avlodga ko'tariladi. Agar 1-avlod o'z byudjetiga yetsa, GC ham 1-avlodni, ham 0-avlodni yig'adi. 1-avlod omon qolganlari 2-avlod ga ko'tariladi.

Managed heap faqat uchta avlodni qo'llab-quvvatlaydi: 0-avlod, 1-avlod va 2-avlod (System.GC.MaxGeneration 2 qaytaradi). CLR initsializatsiya qilinganda barcha uchta avlod uchun byudjetlar tanlaydi. Biroq GC o'z-o'zini sozlaydi (self-tuning) — ilova xotiradan foydalanish shakli asosida byudjetlarni dinamik ravishda o'zgartiradi.

Agar GC 0-avlodni yig'gandan keyin juda kam omon qolganlarni ko'rsa, byudjetni kamaytirishga qaror qilishi mumkin — yig'ishlar tez-tezroq bo'ladi, lekin kamroq ish talab qiladi. Agar barcha 0-avlod obyektlari axlat bo'lsa, GC hech qanday xotirani siqishtirishi kerak bo'lmaydi — NextObjPtr ni 0-avlod boshiga qaytaradi va tamom!

Ilova arxitekturasi

Garbage collector oqimlari ko'pincha stek tepasida bo'sh turgan, keyin biror ish kerak bo'lganda uyg'onib, qisqa muddatli obyektlar yaratib, qaytaruvchi va yana uxlashga tushadigan ilovalar uchun juda yaxshi ishlaydi. GUI ilovalari xabarlar tsiklida o'tiradigan GUI oqimiga ega. Server ilovalari esa thread pool oqimlariga ega. Aksariyat obyektlar qisqa muddatli va hozir axlat.

Avlodli Garbage Collection
Gen 0
Yangi obyektlar
a
b
c
d
e
Gen 1
Bir marta omon qolgan
f
g
h
Gen 2
Uzoq yashovchi
i
j
Yangi obyektlar Gen 0'da yaratiladi. GC'dan omon qolsa Gen 1'ga, keyin Gen 2'ga ko'tariladi. Gen 0 eng tez-tez tozalanadi.

GC Bildirishnoma Klassi

GC avlod byudjetlarini har bir yig'ishdan keyin dinamik o'zgartiradi. Natijada garbage collector ilovangiz xotira yukiga qarab o'z-o'zini avtomatik sozlaydi — bu juda ajoyib!

Quyidagi GCNotification klassi 0-avlod yoki 2-avlod yig'ish sodir bo'lganda hodisani ko'taradi. Bu klassdan foydalanib, yig'ishlar orasida qancha vaqt o'tganini, yig'ishlar orasida qancha xotira ajratilganini hisoblashingiz mumkin:

public static class GCNotification {
    private static Action<Int32> s_gcDone = null;   // Hodisa maydoni

    public static event Action<Int32> GCDone {
        add {
            // Delegatlar hali ro'yxatga olinmagan bo'lsa, bildirishnomalarni boshlash
            if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
            s_gcDone += value;
        }
        remove { s_gcDone -= value; }
    }

    private sealed class GenObject {
        private Int32 m_generation;
        public GenObject(Int32 generation) { m_generation = generation; }
        ~GenObject() {   // Bu Finalize metodi
            // Agar obyekt kerakli avlodda (yoki yuqorida) bo'lsa,
            // delegatlarga GC tugaganini xabar berish
            if (GC.GetGeneration(this) >= m_generation) {
                Action<Int32> temp = Volatile.Read(ref s_gcDone);
                if (temp != null) temp(m_generation);
            }

            // Kamida bitta delegat ro'yxatga olingan bo'lsa,
            // AppDomain tushirilmayotgan bo'lsa va jarayon tugamayotgan bo'lsa
            if ((s_gcDone != null)
                && !AppDomain.CurrentDomain.IsFinalizingForUnload()
                && !Environment.HasShutdownStarted) {
                // Gen 0 uchun yangi obyekt yaratish; Gen 2 uchun qayta ro'yxatga olish
                if (m_generation == 0) new GenObject(0);
                else GC.ReRegisterForFinalize(this);
            } else { /* Obyektlar ketsin */ }
        }
    }
}

Garbage Collection ni Ishga Tushirish Shartlari

Ma'lumki, CLR 0-avlod o'z byudjetini to'ldirganini aniqlasa GC ni ishga tushiradi. Bu eng keng tarqalgan trigger. Biroq qo'shimcha GC triggerlari ham mavjud:

  • Kod System.GC ning statik Collect metodini aniq chaqiradi — Kod CLR dan garbage collection bajarishni aniq so'rashi mumkin. Microsoft bunday so'rovlarni qat'iy taqiqlasa ham, ba'zi hollarda ilovada yig'ishni majburlash mantiqiy bo'lishi mumkin. Buni bobda keyinroq muhokama qilaman.
  • Windows past xotira sharoitini xabar qilmoqda — CLR ichki tarzda Win32 CreateMemoryResourceNotification va QueryMemoryResourceNotification funksiyalaridan foydalanadi. Agar Windows past xotira haqida xabar bersa, CLR jarayonning ishchi to'plamini kamaytirish maqsadida o'lik obyektlarni bo'shatishga garbage collection ni amalga oshiradi.
  • CLR AppDomain ni tushirmoqda — AppDomain tushirilganida, CLR AppDomain dagi hech narsani ildiz deb hisoblamaydi va barcha avlodlardan iborat garbage collection bajariladi.
  • CLR o'chmoqda — CLR jarayon normal tugash paytida o'chadi. O'chish paytida CLR jarayondagi hech narsani ildiz deb hisoblamaydi; obyektlarga tozalash imkoniyati beriladi, lekin CLR xotirani siqishtirish yoki bo'shatishga urinmaydi, chunki jarayon tugamoqda va Windows barcha xotirani qaytarib oladi.

Katta Obyektlar (Large Object Heap)

CLR har bir obyektni kichik obyekt yoki katta obyekt deb hisoblaydi. Bugungi kunda katta obyekt 85,000 bayt yoki undan katta. CLR katta obyektlarga kichik obyektlarga qaraganda biroz boshqacha muomala qiladi:

  • Katta obyektlar kichik obyektlar bilan bir xil manzil maydonida emas, jarayonning manzil maydonidagi boshqa joyda joylashtiriladi.
  • Bugungi kunda GC katta obyektlarni siqishtirmaydi, chunki ularni xotirada ko'chirish uchun juda ko'p vaqt kerak. Shu sababli, katta obyektlar orasida manzil maydoni fragmentatsiyasi yuz berishi mumkin va OutOfMemoryException tashlanishi mumkin. CLR ning kelajakdagi versiyasida katta obyektlar ham siqishtirishda qatnashishi mumkin.
  • Katta obyektlar darhol 2-avlodning bir qismi deb hisoblanadi; ular hech qachon 0 yoki 1-avlodda bo'lmaydi. Shuning uchun katta obyektlarni faqat uzoq yashashi kerak bo'lgan resurslar uchun yarating. Qisqa muddatli katta obyektlarni ajratish 2-avlodni tez-tezroq yig'ilishiga olib keladi va ishlashga zarar yetkazadi.

Odatda katta obyektlar katta satrlar (XML yoki JSON kabi) yoki I/O operatsiyalari uchun ishlatiladigan bayt massivlari bo'ladi.

Maslahat

Ko'pincha katta obyektlar sizga shaffof; ularning mavjudligini e'tiborsiz qoldiring, toki dasturingizda tushunarsiz holat (manzil maydoni fragmentatsiyasi kabi) bilan duch kelguningizcha.

Garbage Collection Rejimlari

CLR ishga tushganda, GC rejimini tanlaydi va bu rejim jarayon umri davomida o'zgarmaydi. Ikkita asosiy GC rejimi mavjud:

  • Workstation — Bu rejim mijoz tomondagi ilovalar uchun garbage collector ni sozlaydi. Past kechikishli (low-latency) GC lar uchun optimallashtirilgan bo'lib, ilova oqimlari to'xtatilish vaqtini minimallashtirib, foydalanuvchini bezovta qilmaydi. Bu rejimda GC mashinada boshqa ilovalar ham ishlashini va CPU resurslarini egallamaslikni taxmin qiladi.
  • Server — Bu rejim server tomondagi ilovalar uchun garbage collector ni sozlaydi. O'tkazuvchanlik va resurslardan foydalanish uchun optimallashtirilgan. Bu rejimda GC mashinadagi barcha CPU lar GC ni tez yakunlashga yordam berish uchun foydalanilishini taxmin qiladi. GC managed heap ni bir nechta bo'limlarga ajratadi — har bir CPU uchun bittadan. Yig'ish boshlanganida, garbage collector har bir CPU uchun bitta maxsus oqim ajratadi; har bir oqim o'z bo'limini boshqa oqimlar bilan parallel yig'adi.

Standart holatda ilovalar Workstation GC rejimida ishlaydi. Server ilovalar (ASP.NET yoki SQL Server kabi) CLR dan Server GC ni yuklashni so'rashi mumkin. Konfiguratsiya fayli orqali ham o'rnatish mumkin:

<configuration>
    <runtime>
        <gcServer enabled="true"/>
    </runtime>
</configuration>

Ilova ishlayotganida CLR dan Server GC rejimida ishlayotganini tekshirish mumkin:

using System;
using System.Runtime; // GCSettings shu nomlar fazosida

public static class Program {
    public static void Main() {
        Console.WriteLine("Application is running with server GC=" + GCSettings.IsServerGC);
    }
}

Kechikish Rejimlari (Latency Modes)

Ikkala rejimda ham GC ikkita pastki rejimda ishlashi mumkin: concurrent (standart) va non-concurrent. Concurrent rejimda GC ilovangiz ishlashda davom etayotganda fon oqimida obyektlarni belgilaydigan qo'shimcha oqimga ega.

Ilova GCSettings klassining GCLatencyMode xususiyatini o'rnatib, garbage collection ustidan ba'zi nazoratga ega bo'lishi mumkin. Bu xususiyat GCLatencyMode ro'yxatlangan turning quyidagi qiymatlariga o'rnatilishi mumkin:

Belgi nomiTavsif
BatchConcurrent GC ni o'chiradi. Server GC uchun standart.
InteractiveConcurrent GC ni yoqadi. Workstation GC uchun standart.
LowLatencyQisqa muddatli, vaqtga sezgir operatsiyalar (animatsiya chizish kabi) paytida ishlating, bunda 2-avlod yig'ish buzilishi mumkin.
SustainedLowLatencyIlovangiz bajarilishining asosiy qismi uchun uzoq GC pauzalaridan qochish uchun ishlating. Bu sozlash xotira mavjud bo'lganda barcha blokirovka qiluvchi 2-avlod yig'ishlarning sodir bo'lishini oldini oladi.

LowLatency rejimida OutOfMemoryException olish ehtimoli oshadi. Shuning uchun bu rejimda imkon qadar qisqa vaqt qoling, ko'p obyekt ajratishdan saqlaning va katta obyektlar ajratishdan qoching. Keyin rejimni Batch yoki Interactive ga qaytaring.

private static void LowLatencyDemo() {
    GCLatencyMode oldMode = GCSettings.LatencyMode;
    System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
    try {
        GCSettings.LatencyMode = GCLatencyMode.LowLatency;
        // Vaqtga sezgir kodingizni shu yerda ishga tushiring...
    }
    finally {
        GCSettings.LatencyMode = oldMode;
    }
}

Garbage Collection ni Dasturiy Boshqarish

System.GC turi ilovangizga garbage collector ustidan biroz to'g'ridan-to'g'ri nazoratni beradi. GC.MaxGeneration xususiyati orqali managed heap tomonidan qo'llab-quvvatlanadigan maksimal avlodni so'rashingiz mumkin — bu xususiyat doimo 2 qaytaradi.

Shuningdek, GC.Collect metodini chaqirib yig'ishni majburlashingiz mumkin. Eng murakkab overloadning imzosi:

void Collect(Int32 generation, GCCollectionMode mode, Boolean blocking);

GCCollectionMode turi quyidagi qiymatlarga ega:

Belgi nomiTavsif
DefaultGC.Collect ni bayroqsiz chaqirish bilan bir xil. Bugungi kunda Forced bilan teng.
ForcedBelgilangan avlodgacha barcha avlodlar uchun darhol yig'ishni majburlaydi.
OptimizedGarbage collector faqat yig'ish samarali bo'lsa (ko'p xotira bo'shatish yoki fragmentatsiyani kamaytirish orqali) yig'ishni bajaradi. Aks holda, chaqirish hech qanday ta'sir ko'rsatmaydi.
Diqqat

Ko'p hollarda Collect metodini chaqirishdan saqlaning; garbage collector ni o'z holicha ishlashiga va avlod byudjetlarini haqiqiy ilova xatti-harakati asosida sozlashiga yo'l qo'yish yaxshiroqdir. Biroq, konsol yoki GUI ilova yozayotgan bo'lsangiz, GCCollectionMode.Optimized bilan ba'zi paytlarda yig'ish taklif qilishingiz mumkin. Default va Forced rejimlari debugging, test va xotira oqishlarini qidirish uchun ishlatiladi.

Masalan, takrorlanmaydigan hodisa (masalan, ilova initsializatsiyasi yoki foydalanuvchi ma'lumotlar faylini saqlashi) ko'p eski obyektlarning o'lishiga sabab bo'lgan bo'lsa, Collect ni chaqirishni o'ylash mumkin. Chunki GC ning o'tmishga asoslangan bashoratlari takrorlanmaydigan hodisalar uchun to'g'ri bo'lmasligi mumkin.

Server ilovalari uchun GC klassi RegisterForFullGCNotification metodini taklif qiladi. WaitForFullGCApproach, WaitForFullGCComplete va CancelFullGCNotification helper metodlari bilan ilova garbage collector to'liq yig'ishga yaqinlashayotganini bilib olishi mumkin va qulayroq vaqtda GC.Collect ni chaqirishi yoki yukni boshqa serverga yo'naltirishi mumkin.

Ilovangiz Xotira Foydalanishini Monitoring Qilish

Jarayon ichida garbage collector ni monitoring qilish uchun chaqirishingiz mumkin bo'lgan bir nechta metodlar mavjud. GC klassi quyidagi statik metodlarni taklif qiladi:

Int32 CollectionCount(Int32 generation);
Int64 GetTotalMemory(Boolean forceFullCollection);

Muayyan kod blokini profillash uchun bu metodlarni kod blokidan oldin va keyin chaqirib, farqni hisoblang. Bu sizga kod bloki ishchi to'plamga qanchalik ta'sir qilgani va nechta garbage collection sodir bo'lgani haqida yaxshi ko'rsatkich beradi.

.NET Framework o'rnatilganda ishlash ko'rsatkichlari (performance counters) ham o'rnatiladi. .NET CLR Memory ishlash obyektini PerfMon.exe yoki System Monitor ActiveX boshqaruvi orqali ko'rishingiz mumkin. Monitoring qiladigan hisoblagichlarni tanlang va tizim real-vaqtda grafiklarni chizadi.

Xotira va ishlashni tahlil qilish uchun yana bir ajoyib vosita PerfView dir. Bu vosita Windows uchun Hodisalarni Kuzatish (ETW) jurnallarini yig'ib, ularni qayta ishlaydi. SOS Debugging Extension (SOS.dll) ham xotira muammolarini debugging qilishda katta yordam beradi — jarayon ichidagi managed heapga qancha xotira ajratilganini, finalization navbatidagi barcha obyektlarni, GCHandle jadvalidagi yozuvlarni va boshqalarni ko'rish imkonini beradi.

Maxsus Tozalash Kerak Turlar Bilan Ishlash

Shu paytgacha siz garbage collection va managed heap haqida asosiy tushunchaga ega bo'ldingiz. Yaxshiyamki, ko'pchilik turlar ishlashi uchun faqat xotira kerak. Biroq ba'zi turlar foydalanish uchun xotiraga qo'shimcha ravishda native resursdan ham foydalanishi kerak.

System.IO.FileStream turi, masalan, faylni ochishi (native resurs) va uning handle ini saqlashi kerak. System.Threading.Mutex turi Windows mutex kernel obyektini (native resurs) ochadi va uning handle ini saqlaydi.

Agar native resursni o'rab turgan turning GC tomonidan xotirasi qaytarib olinsa, GC managed heap dagi obyekt ishlatgan xotirani qaytarib oladi; lekin native resurs GC bilmaydigan narsa bo'lib, oqib ketadi. Bu aniq istalmagan holat, shuning uchun CLR finalization (yakunlash) deb nomlangan mexanizmni taklif qiladi.

Finalization (Yakunlash)

System.Object Finalize deb nomlangan protected va virtual metodni aniqlaydi. Garbage collector obyekt axlat ekanligini aniqlasa, obyektning Finalize metodini chaqiradi (agar u qayta yozilgan bo'lsa). C# da Finalize metodini sinf nomi oldida tilda (~) belgisi qo'yib aniqlanadi:

internal sealed class SomeType {
    // Bu Finalize metodi
    ~SomeType() {
        // Bu yerdagi kod Finalize metodi ichida
    }
}
Muhim

C++ bilan tanish bo'lsangiz, C# Finalize metodi uchun maxsus sintaksis C++ destruktor sintaksisiga o'xshashligini sezasiz. Aslida C# dasturlash tili spetsifikatsiyasi bu metodga destruktor deb murojaat qiladi. Biroq Finalize metodi C++ destruktoriga o'xshamaydi va bu tildan boshqa tilga o'tayotgan dasturchilar uchun katta chalkashlik tug'dirgan. Muammo shundaki, dasturchilar C# destruktor sintaksisidan foydalanib, turning obyektlari leksik ko'rinish doirasidan chiqishda deterministik tarzda yo'q qilinishini kutishadi. Biroq CLR deterministik yo'q qilishni qo'llab-quvvatlamaydi.

Finalize metodlari GC tomonidan axlat deb aniqlangan obyektlarda chaqiriladi. Bu degani bu obyektlar uchun xotira darhol qaytarib olinmaydi, chunki Finalize metodi maydonlarga murojaat qiluvchi kodni bajarishi mumkin. Finalizable obyekt yig'ishdan omon qolishi kerak bo'lganligi sababli, u keyingi avlodga ko'tariladi va obyektni uzoqroq yashashga majbur qiladi. Shu sababli imkon qadar finalizationdan qoching.

Bundan tashqari, Finalize metodlari qaysi tartibda chaqirilishi haqida hech qanday kafolat yo'q. Shuning uchun Finalize metodi ichida turi Finalize metodini aniqlaydigan boshqa obyektlarga murojaat qilmang — ular allaqachon yakunlangan bo'lishi mumkin.

CLR Finalize metodlarini chaqirish uchun maxsus yuqori ustuvor oqimdan foydalanadi. Agar Finalize metodi bloklansa (masalan, cheksiz tsiklga kirsa yoki hech qachon signallanmaydigan obyektni kutsa), bu maxsus oqim boshqa Finalize metodlarini chaqira olmaydi — ilova ishlayotgan davomida xotira oqib ketadi.

SafeHandle Klassi

Agar siz native resursni o'rab turgan managed turni yaratayotgan bo'lsangiz, avval System.Runtime.InteropServices.SafeHandle asosiy klassdan hosila bo'ling:

public abstract class SafeHandle : CriticalFinalizerObject, IDisposable {
    // Native resursga handle
    protected IntPtr handle;

    protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) {
        this.handle = invalidHandleValue;
        // Agar ownsHandle true bo'lsa, native resurs bu
        // SafeHandle-hosila obyekt yig'ilganda yopiladi
    }

    protected void SetHandle(IntPtr handle) {
        this.handle = handle;
    }

    // Resursni Dispose chaqirib aniq bo'shatishingiz mumkin
    // Bu IDisposable interfeysining Dispose metodi
    public void Dispose() { Dispose(true); }

    // Hosila klass bu metodni qayta yozib resursni bo'shatadigan kodni yozadi
    protected abstract Boolean ReleaseHandle();

    public void SetHandleAsInvalid() {
        // Resurs bo'shatilganligini ko'rsatadigan bayroqni o'rnatish
        // GC.SuppressFinalize(this) ni chaqirish
    }

    public Boolean IsClosed {
        get {
            // Resurs bo'shatilganligini ko'rsatadigan bayroqni qaytarish
        }
    }

    public abstract Boolean IsInvalid {
        // Hosila klass bu xususiyatni qayta yozadi
        // Handle qiymati resursni ifodalamasa true qaytarishi kerak
        get;
    }

    // Xavfsizlik va havolalarni hisoblash uchun
    public void    DangerousAddRef(ref Boolean success) {...}
    public IntPtr  DangerousGetHandle() {...}
    public void    DangerousRelease() {...}
}

SafeHandle CriticalFinalizerObject dan hosila bo'lgan. CLR bu klass va undan hosila bo'lgan klasslarni juda maxsus tarzda ko'radi. Xususan, CLR uchta ajoyib xususiyatni beradi:

  • CriticalFinalizerObject-hosila turning birinchi obyekti yaratilganda, CLR darhol meros ierarxiyasidagi barcha Finalize metodlarini JIT-kompilyatsiya qiladi. Bu native resursni ajratish mumkin bo'lgani, lekin past xotira sharoitida Finalize metodini kompilyatsiya qilib bo'lmaydigan holatni oldini oladi.
  • CLR CriticalFinalizerObject-hosila turlarning Finalize metodlarini CriticalFinalizerObject dan hosila bo'lmagan turlarning Finalize metodlaridan keyin chaqiradi. Bu managed resurs klasslari o'zlarining Finalize metodlarida CriticalFinalizerObject-hosila obyektlarga muvaffaqiyatli murojaat qilishini ta'minlaydi.
  • Host ilova rudely (qo'pol tarzda) AppDomain ni to'xtatsa ham, CLR CriticalFinalizerObject-hosila turlarning Finalize metodlarini chaqiradi.

Microsoft.Win32.SafeHandles nomlar fazosida SafeHandleZeroOrMinusOneIsInvalid helper klassi mavjud. Undan SafeFileHandle, SafeRegistryHandle, SafeWaitHandle va SafeMemoryMappedViewHandle kabi klasslar hosila bo'lgan:

public sealed class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid {
    public SafeFileHandle(IntPtr preexistingHandle, Boolean ownsHandle)
        : base(ownsHandle) {
        base.SetHandle(preexistingHandle);
    }

    protected override Boolean ReleaseHandle() {
        // Windows ga native resursni yopishni aytish
        return Win32Native.CloseHandle(base.handle);
    }
}

Dispose Pattern — Native Resursni O'rab Turgan Turni Ishlatish

Endi SafeHandle-hosila klassni aniqlay olganingizni bilganingizdan keyin, dasturchi uni qanday ishlatishini ko'rib chiqaylik. System.IO.FileStream klassi faylni ochish, undan o'qish, unga yozish va faylni yopish imkoniyatini beradi.

Aytaylik, vaqtinchalik fayl yaratib, ba'zi baytlarni yozib, keyin faylni o'chirmoqchisiz:

using System;
using System.IO;

public static class Program {
    public static void Main() {
        Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };

        FileStream fs = new FileStream("Temp.dat", FileMode.Create);
        fs.Write(bytesToWrite, 0, bytesToWrite.Length);

        File.Delete("Temp.dat");   // IOException tashlaydi
    }
}

Afsuski, File.Delete muvaffaqiyatsiz bo'ladi, chunki FileStream hali fayl handle ini ushlab turibdi. Yechim — faylni aniq yopish:

Native resurslar umrini boshqarish imkonini beruvchi klasslar IDisposable interfeysini amalga oshiradi:

public interface IDisposable {
    void Dispose();
}
Muhim

Agar klass maydonning turi dispose patternini amalga oshirsa, klassning o'zi ham dispose patternini amalga oshirishi kerak. Dispose metodi maydon tomonidan ishora qilingan obyektni dispose qilishi kerak.

FileStream klassi IDisposable interfeysini amalga oshiradi. Endi kodimizni tuzatamiz:

using System;
using System.IO;

public static class Program {
    public static void Main() {
        Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };

        FileStream fs = new FileStream("Temp.dat", FileMode.Create);
        fs.Write(bytesToWrite, 0, bytesToWrite.Length);

        // Yozish tugagach faylni aniq yopish
        fs.Dispose();

        // Faylni o'chirish. Bu endi doimo ishlaydi.
        File.Delete("Temp.dat");
    }
}
Diqqat

Umuman olganda, men Dispose ni aniq chaqirishni qat'iy taqiqlayman. Sababi: CLR ning garbage collector juda yaxshi yozilgan va unga o'z ishini qilishga yo'l qo'yishingiz kerak. GC obyekt ilova kodidan endi erishib bo'lmaydigan bo'lganini biladi va faqat shunda uni yig'adi. Men Dispose ni faqat kodingizda resursni tozalash kerakligini aniq biladigan joylarda chaqirishni tavsiya qilaman (masalan, ochiq faylni o'chirishga urinishda).

C# ning using Operatori

Agar Dispose ni aniq chaqirishga qaror qilsangiz, chaqiruvni finally blokiga joylashtirishni qat'iy tavsiya qilaman. Shunda tozalash kodi albatta bajariladi:

using System;
using System.IO;

public static class Program {
    public static void Main() {
        Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };

        FileStream fs = new FileStream("Temp.dat", FileMode.Create);
        try {
            fs.Write(bytesToWrite, 0, bytesToWrite.Length);
        }
        finally {
            // Yozish tugagach faylni aniq yopish
            if (fs != null) fs.Dispose();
        }

        File.Delete("Temp.dat");
    }
}

Yaxshiyamki, C# tili using operatorini taklif qiladi — bu yuqoridagi kodga teng soddalashtirilgan sintaksis:

using System;
using System.IO;

public static class Program {
    public static void Main() {
        Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };

        // Vaqtinchalik faylni yaratish
        using (FileStream fs = new FileStream("Temp.dat", FileMode.Create)) {
            // Vaqtinchalik faylga baytlarni yozish
            fs.Write(bytesToWrite, 0, bytesToWrite.Length);
        }

        // Faylni o'chirish
        File.Delete("Temp.dat");
    }
}

using operatorida siz obyektni initsializatsiya qilasiz va uni o'zgaruvchida saqlaysiz. Kompilyator bu kodni kompilyatsiya qilganda, avtomatik ravishda try va finally bloklarini yaratadi. finally bloki ichida kompilyator obyektni IDisposable ga cast qiladi va Dispose ni chaqiradi. Shuning uchun using operatori faqat IDisposable interfeysini amalga oshirgan turlar bilan ishlatilishi mumkin.

Eslatma

C# ning using operatori bir nechta o'zgaruvchilarni (bir xil turdagi) initsializatsiya qilish imkonini beradi. Shuningdek, allaqachon initsializatsiya qilingan o'zgaruvchini ham ishlatish mumkin.

Muhim

IDisposable interfeysini amalga oshiradigan o'z turingizni aniqlashda, barcha metodlar va xususiyatlarda obyekt aniq tozalanganligi tekshirilishi va System.ObjectDisposedException tashlanishi kerak. Dispose metodi hech qachon istisno tashlamasligi kerak; agar u bir necha marta chaqirilsa, shunchaki qaytishi kerak.

Native Resurslar Bilan Boshqa GC Xususiyatlari

Ba'zida native resurs ko'p xotira egallaydi, lekin uni o'rab turgan managed obyekt juda kichik. Klassik misol — bitmap. Bitmap bir necha megabayt native xotirani egallashi mumkin, lekin managed obyekt kichik (faqat HBITMAP — 4 yoki 8 baytlik qiymatni saqlaydi). CLR nuqtai-nazaridan jarayon yig'ishni amalga oshirmasdan yuzlab bitmaplarni ajratishi mumkin. Buni tuzatish uchun GC klassi ikkita statik metodni taklif qiladi:

public static void AddMemoryPressure(Int64 bytesAllocated);
public static void RemoveMemoryPressure(Int64 bytesAllocated);

Potensial ravishda katta native resursni o'rab turuvchi klass bu metodlardan foydalanib, garbage collector ga qancha xotira haqiqatan ham ishlatilayotgani haqida maslahat berishi kerak.

Sonli cheklangan native resurslar uchun System.Runtime.InteropServices nomlar fazosi HandleCollector klassini taklif qiladi:

public sealed class HandleCollector {
    public HandleCollector(String name, Int32 initialThreshold);
    public HandleCollector(String name, Int32 initialThreshold, Int32 maximumThreshold);
    public void Add();
    public void Remove();

    public Int32 Count { get; }
    public Int32 InitialThreshold { get; }
    public Int32 MaximumThreshold { get; }
    public String Name { get; }
}

Cheklangan miqdordagi native resursni o'rab turuvchi klass bu klassning instansiyasidan foydalanib, garbage collector ga resursning qancha instansiyasi haqiqatan ham ishlatilayotgani haqida maslahat berishi kerak. Ichki tarzda bu klass hisobni kuzatadi va u yuqori bo'lganda, garbage collection majburlaydi.

Finalization Ichki Ishlashi

Tashqi tomondan finalization oddiy ko'rinadi: siz obyekt yaratasiz va u yig'ilganda uning Finalize metodi chaqiriladi. Lekin ichkarida finalization ancha murakkab.

Ilova yangi obyekt yaratganda, new operatori heap dan xotirani ajratadi. Agar obyektning turi Finalize metodini aniqlasa, turning instansiya konstruktori chaqirilishidan oldin obyektga ko'rsatkich finalization ro'yxati (finalization list) ga qo'yiladi. Finalization ro'yxati — garbage collector tomonidan boshqariladigan ichki ma'lumotlar strukturasi. Ro'yxatdagi har bir yozuv xotirasi qaytarib olinishidan oldin Finalize metodi chaqirilishi kerak bo'lgan obyektga ishora qiladi.

Eslatma

System.Object Finalize metodini aniqlasa ham, CLR uni e'tiborsiz qoldirishni biladi; ya'ni, turning Finalize metodi System.Object dan meros olingan bo'lsa, obyekt finalizable hisoblanmaydi. Hosila turlardan biri Object ning Finalize metodini qayta yozishi kerak.

Garbage collection sodir bo'lganda va garbage obyektlar aniqlanganida, GC finalization ro'yxatini skanerlaydi. Agar axlat obyektga havola topilsa, havola finalization ro'yxatidan olib tashlanib, freachable navbat (freachable queue) ga qo'shiladi. Freachable navbat — garbage collector ning yana bir ichki ma'lumotlar strukturasi. Bu navbatdagi har bir havola Finalize metodi chaqirilishi kerak bo'lgan obyektni belgilaydi.

Freachable navbatning nomi "f" (finalization uchun) va "reachable" (erishiladigan) dan tashkil topgan. Sababi: freachable navbatdagi har bir havola ildiz hisoblanadi — xuddi statik maydonlar kabi. Shuning uchun freachable navbatdagi havola obyektni erishiladigan deb belgilaydi va u axlat emas.

Qisqasi, obyekt erishib bo'lmaydigan bo'lganda, garbage collector uni axlat deb hisoblaydi. Keyin GC obyektning havolasini finalization ro'yxatidan freachable navbatga ko'chirganda, obyekt endi axlat emas va uning xotirasi qaytarib olinmaydi. Agar obyekt axlat bo'lib, keyin axlat bo'lmasa, biz bu obyekt tiriltildi (resurrected) deb aytamiz.

Freachable navbatda yozuvlar paydo bo'lganda, maxsus yuqori ustuvor finalization oqimi uyg'onadi, har bir yozuvni navbatdan olib, har bir obyektning Finalize metodini chaqiradi. Keyingi safar garbage collector eski avlodda ishga tushganda, yakunlangan obyektlar haqiqatan ham axlat ekanini ko'radi (ilova ildizlari ham, freachable navbat ham ularga ishora qilmaydi) va ularning xotirasi qaytarib olinadi.

Muhim xulosalar: finalization talab qiladigan obyektlar uchun xotirani qaytarib olish kamida ikki garbage collection ni talab qiladi. Amaliyotda, ikki yig'ishdan ko'proq kerak bo'ladi, chunki obyektlar boshqa avlodga ko'tariladi.

Obyektlar Umrini Monitoring va Qo'lda Boshqarish

CLR har bir AppDomain ga GC handle jadvali ni beradi. Bu jadval ilovaga obyektning umrini monitoring qilish yoki qo'lda boshqarish imkonini beradi. AppDomain yaratilganda, jadval bo'sh bo'ladi. Jadvaldagi har bir yozuv managed heap dagi obyektga havola va u bilan qanday muomala qilishni ko'rsatuvchi bayroqdan iborat.

GCHandle Strukturasi

Ilova jadvalga yozuvlarni System.Runtime.InteropServices.GCHandle turi orqali qo'shadi va olib tashlaydi:

// Bu tur System.Runtime.InteropServices nomlar fazosida aniqlangan
public struct GCHandle {
    // Jadvalda yozuv yaratadigan statik metodlar
    public static GCHandle Alloc(object value);
    public static GCHandle Alloc(object value, GCHandleType type);

    // GCHandle ni IntPtr ga aylantiradigan statik metodlar
    public static explicit operator IntPtr(GCHandle value);
    public static IntPtr ToIntPtr(GCHandle value);

    // IntPtr ni GCHandle ga aylantiradigan statik metodlar
    public static explicit operator GCHandle(IntPtr value);
    public static GCHandle FromIntPtr(IntPtr value);

    // Ikkita GCHandle ni solishtiradigan statik metodlar
    public static Boolean operator ==(GCHandle a, GCHandle b);
    public static Boolean operator !=(GCHandle a, GCHandle b);

    // Jadvaldagi yozuvni bo'shatadigan instansiya metodi
    public void Free();

    // Yozuvning obyekt havolasini olish/o'rnatish uchun instansiya xususiyati
    public object Target { get; set; }

    // Indeks 0 bo'lmasa true qaytaradigan instansiya xususiyati
    public Boolean IsAllocated { get; }

    // Pinned yozuv uchun obyektning manzilini qaytaradi
    public IntPtr AddrOfPinnedObject();
}

Obyektning umrini boshqarish yoki monitoring qilish uchun GCHandle ning statik Alloc metodini chaqirasiz, monitoring/boshqarish qilmoqchi bo'lgan obyektga havolani va qanday monitoring/boshqarish qilishni ko'rsatuvchi GCHandleType bayrog'ini uzatasiz:

public enum GCHandleType {
    Weak = 0,                // Obyekt mavjudligini monitoring qilish
    WeakTrackResurrection = 1, // Obyekt mavjudligini monitoring qilish
    Normal = 2,              // Obyekt umrini boshqarish
    Pinned = 3               // Obyekt umrini boshqarish
}

Har bir bayroqning ma'nosi:

  • Weak — Obyektning umrini monitoring qilish imkonini beradi. Garbage collector obyektni ilova kodidan erishib bo'lmaydigan deb aniqlasa, buni aniqlashingiz mumkin. E'tibor bering, Finalize metodi bajarilgan yoki bajarilmagan bo'lishi mumkin va obyekt hali xotirada bo'lishi mumkin.
  • WeakTrackResurrection — Obyektning umrini monitoring qilish imkonini beradi. Garbage collector obyektni erishib bo'lmaydigan deb aniqlasa va Finalize metodi (agar mavjud bo'lsa) bajarilgan, va obyektning xotirasi qaytarib olingan bo'lsa, buni aniqlashingiz mumkin.
  • Normal — Obyektning umrini boshqarish imkonini beradi. Ilovada bu obyektga ishora qiluvchi ildizlar bo'lmasa ham, obyekt xotirada qolishi kerakligini garbage collector ga aytasiz. Obyektning xotirasi siqishtirilishi (ko'chirilishi) mumkin.
  • Pinned — Obyektning umrini boshqarish imkonini beradi. Ildizlar bo'lmasa ham obyekt xotirada qolishi kerak va xotirasi siqishtirilishi mumkin emas. Bu odatda xotira manzilini native kodga uzatmoqchi bo'lganda foydalidir — native kod managed heap dagi bu xotiraga GC uni ko'chirmasligini bilib yozishi mumkin.

Garbage collection sodir bo'lganda:

  1. GC barcha erishiladigan obyektlarni belgilaydi. Keyin GC handle jadvalni skanerlaydi; barcha Normal yoki Pinned obyektlar ildiz sifatida ko'rib chiqiladi va belgilanadi.
  2. GC barcha Weak yozuvlarni skanerlaydi. Agar Weak yozuv belgilanmagan obyektga ishora qilsa, havola null ga o'zgartiriladi.
  3. GC finalization ro'yxatini skanerlaydi. Belgilanmagan obyektlar freachable navbatga ko'chiriladi va endi erishiladigan deb belgilanadi.
  4. GC barcha WeakTrackResurrection yozuvlarni skanerlaydi. Agar belgilanmagan obyektga ishora qilsa (freachable navbatdagi endi belgilangan obyekt ham bu yerda hisobga olinadi), havola null ga o'zgartiriladi.
  5. GC xotirani siqishtiradi. Pinned obyektlar ko'chirilmaydi; GC ular atrofida boshqa obyektlarni ko'chiradi.

C# fixed operatori ham obiektni pin qiladi va bu ba'zan GCHandle ajratishdan samaraliroq:

unsafe public static void Go() {
    // Darhol axlat bo'ladigan bir qancha obyektlar ajratish
    for (Int32 x = 0; x < 10000; x++) new Object();

    IntPtr originalMemoryAddress;
    Byte[] bytes = new Byte[1000];   // Axlat obyektlardan keyin ajratish

    // Byte[] ning xotiradagi manzilini olish
    fixed (Byte* pbytes = bytes) { originalMemoryAddress = (IntPtr) pbytes; }

    // Yig'ishni majburlash; axlat obyektlar ketadi va Byte[] siqishtirilishi mumkin
    GC.Collect();

    // Byte[] ning endi xotiradagi manzilini olish va birinchi manzil bilan solishtirish
    fixed (Byte* pbytes = bytes) {
        Console.WriteLine("The Byte[] did{0} move during the GC",
            (originalMemoryAddress == (IntPtr) pbytes) ? " not" : null);
    }
}

Weak References (Zaif Havolalar)

GCHandle turi bilan ishlash biroz og'ir bo'lishi va yuqori xavfsizlik ruxsatnomasini talab qilishi mumkinligi sababli, System nomlar fazosi sizga yordam beradigan WeakReference<T> klassini o'z ichiga oladi:

public sealed class WeakReference<T> : ISerializable where T : class {
    public WeakReference(T target);
    public WeakReference(T target, Boolean trackResurrection);
    public void SetTarget(T target);
    public Boolean TryGetTarget(out T target);
}

Bu klass aslida GCHandle instansiyasi atrofidagi obyektga yo'naltirilgan o'rov: mantiqiy ravishda uning konstruktori GCHandle.Alloc ni chaqiradi, TryGetTarget metodi GCHandle.Target xususiyatini so'raydi, SetTarget metodi GCHandle.Target xususiyatini o'rnatadi, va Finalize metodi GCHandle.Free ni chaqiradi. WeakReference<T> faqat zaif havolalarni qo'llab-quvvatlaydi (Normal yoki Pinned emas). Kamchiligi — bu klass heapda instansiyani ajratishi kerak, shuning uchun GCHandle instansiyasiga qaraganda og'irroq.

Aytaylik, Object-A vaqti-vaqti bilan Object-B ning metodini chaqiradi. Object-A ning Object-B ga havolasi Object-B ni garbage collect qilinishdan to'sadi. Ba'zi noyob holatlarda bu istalmagan bo'lishi mumkin. Buning o'rniga, Object-A GCHandle.Alloc metodini Weak bayrog'i bilan chaqirib, Object-B ga zaif havolani saqlashi mumkin.

Object-A Object-B ning metodini chaqirmoqchi bo'lganda, GCHandle ning Target xususiyatini so'raydi. Agar null bo'lmagan qiymat qaytarsa — Object-B hali tirik va metodni chaqirish mumkin. Agar Target null qaytarsa — Object-B yig'ilgan va Object-A bu haqda xabar qilinadi.

Muhim — Weak References va Keshlash

Dasturchilar zaif havolalar haqida bilib, ularni keshlash scenariylarida ishlatish mumkinligi haqida o'ylashadi. Masalan, ko'p ma'lumotli obyektlarni yaratib, ularga zaif havolalar saqlash. Ma'lumot kerak bo'lganda zaif havolani tekshirish — agar obyekt hali mavjud bo'lsa, uni ishlatish; agar GC yig'ib olgan bo'lsa, qayta yaratish.

Muammo shundaki: garbage collectionlar faqat xotira to'lganda emas, 0-avlod byudjeti to'lganda ham sodir bo'ladi. Shuning uchun obyektlar kerakli bo'lganidan ko'proq tashlab yuboriladi va ilovangiz ishlashi jiddiy pasayadi.

Zaif havolalar keshda samarali ishlatilishi mumkin, lekin yaxshi kesh algoritmi — xotira sarfi va tezlik o'rtasidagi to'g'ri muvozanatni topadigan — juda murakkab. Asosan siz barcha obyektlaringizga kuchli havolalar saqlamoqchisiz, keyin xotira tanglashayotganini ko'rganda kuchli havolalarni zaif havolalarga aylantira boshlaysiz.

ConditionalWeakTable Klassi

Dasturchilar tez-tez ma'lumotni boshqa entitet bilan bog'lashni xohlashadi. Masalan, ma'lumotni oqim bilan, AppDomain bilan yoki alohida obyekt bilan bog'lash. System.Runtime.CompilerServices.ConditionalWeakTable<TKey,TValue> klassi alohida obyekt bilan ma'lumot bog'lash imkonini beradi:

public sealed class ConditionalWeakTable<TKey, TValue>
    where TKey : class where TValue : class {
    public ConditionalWeakTable();
    public void Add(TKey key, TValue value);
    public TValue GetValue(TKey key, CreateValueCallback<TKey, TValue> createValueCallback);
    public Boolean TryGetValue(TKey key, out TValue value);
    public TValue GetOrCreateValue(TKey key);
    public Boolean Remove(TKey key);

    public delegate TValue CreateValueCallback(TKey key); // Ichki delegat ta'rifi
}

Ma'lumotni bir yoki bir nechta obyekt bilan bog'lamoqchi bo'lsangiz, avval bu klassning instansiyasini yaratasiz. Keyin Add metodini chaqirib, biror obyektga havolani (key parametr) va u bilan bog'lamoqchi bo'lgan ma'lumotni (value parametr) uzatasiz.

Jadval ichki tarzda key sifatida uzatilgan obyektga WeakReference saqlaydi — bu jadval obyektni tirik tutishga majburlamasligini ta'minlaydi. Lekin ConditionalWeakTable ni maxsus qiladigan narsa: u key tomonidan aniqlangan obyekt xotirada bo'lganda value ham xotirada qolishini kafolatlaydi. Bu oddiy WeakReference dan ko'proq, chunki oddiy WeakReference da value key tirik bo'lsa ham garbage collect qilinishi mumkin. ConditionalWeakTable XAML tomonidan ishlatiladigan dependency property mexanizmini amalga oshirish uchun ishlatilishi mumkin. Dinamik tillar ham buni obyektlar bilan ma'lumotni dinamik bog'lash uchun ichki ishlatadi.

Quyida ConditionalWeakTable dan foydalanish namunasi — har qanday obyektga GCWatch extension metodini chaqirib, u garbage collect qilinganida konsolda xabar beradi:

internal static class ConditionalWeakTableDemo {
    public static void Main() {
        Object o = new Object().GCWatch("My Object created at " + DateTime.Now);
        GC.Collect();        // Bu yerda GC bildirishnomasini ko'rmaymiz
        GC.KeepAlive(o);     // o ishora qilgan obyekt shu yerigacha tirik
        o = null;            // o ishora qilgan obyekt endi o'lishi mumkin

        GC.Collect();        // Bu satrdan keyin GC bildirishnomasini ko'ramiz
        Console.ReadLine();
    }
}

internal static class GCWatcher {
    // Stringlar bilan ehtiyot bo'ling chunki interning va MarshalByRefObject proksi obyektlari
    private readonly static ConditionalWeakTable<Object, NotifyWhenGCd<String>> s_cwt =
        new ConditionalWeakTable<Object, NotifyWhenGCd<String>>();

    private sealed class NotifyWhenGCd<T> {
        private readonly T m_value;
        internal NotifyWhenGCd(T value) { m_value = value; }
        public override string ToString() { return m_value.ToString(); }
        ~NotifyWhenGCd() { Console.WriteLine("GC'd: " + m_value); }
    }

    public static T GCWatch<T>(this T @object, String tag) where T : class {
        s_cwt.Add(@object, new NotifyWhenGCd<String>(tag));
        return @object;
    }
}