Assembly.Unload ti uopšte nije potreban i u krajnjoj instanci narušava koncept managed code-a. AppDomain može da se unload-uje samo zato što je "application boundary" ujedno i "garbage collector boundary". Elem, da bih dokazao da sam u pravu ;), sklopio sam ovo malo parče koda (šta bih inače radio nedeljom popodne :) ). Pošto se assembly (i njegov image) tretira kao tip i samim tim alocira na heapu, onda možemo uraditi i nešto ovako:
Code:
using System;
using System.Reflection;
namespace GCCollectIntExample
{
class MyGCCollectClass
{
private const long maxGarbage = 100000;
static void Main()
{
MyGCCollectClass myGCCol = new MyGCCollectClass();
Console.WriteLine("--- Begin");
Console.WriteLine("Allocated memory is {0}", GC.GetTotalMemory(false));
Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));
Console.WriteLine("--- Loading assembly");
Assembly asm;
asm = Assembly.LoadFile(@"c:\included.dll");
asm = null;
Console.WriteLine("--- Loaded");
Console.WriteLine("Allocated memory is {0}", GC.GetTotalMemory(false));
Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));
Console.WriteLine("--- Loading and dereferencing {0} times", maxGarbage);
for(int i = 0; i < maxGarbage; i++)
{
// Fill up memory with unused assemblies.
asm = Assembly.LoadFile(@"c:\included.dll");
asm = null;
}
Console.WriteLine("--- Done");
Console.WriteLine("Allocated memory is {0}", GC.GetTotalMemory(false));
Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));
Console.WriteLine("--- Full collect of all generations");
GC.Collect();
Console.WriteLine("--- Done");
Console.WriteLine("Allocated memory is {0}", GC.GetTotalMemory(false));
Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));
Console.Read();
}
}
}
Za 10 iteracija rezultat je:
Code:
--- Begin
Allocated memory is 24496
Generation: 0
--- Loading assembly
--- Loaded
Allocated memory is 40880
Generation: 0
--- Loading and dereferencing 10 times
--- Done
Allocated memory is 49072
Generation: 0
--- Full collect of all generations
--- Done
Allocated memory is 30448
Generation: 1
Dok je za 100000 iteracija (što je trajalo sveukupno oko 20s):
Code:
--- Begin
Allocated memory is 24496
Generation: 0
--- Loading assembly
--- Loaded
Allocated memory is 40880
Generation: 0
--- Loading and dereferencing 100000 times
--- Done
Allocated memory is 310168
Generation: 1
--- Full collect of all generations
--- Done
Allocated memory is 30632
Generation: 2
Iako se ne može napraviti neka hirurški precizna analiza neke stvari se mogu zaključiti iz ovoga. Ali kao prvo malo pojašnjenje oko "Generation: x", ovde program ispisuje koliko puta je GC pokušao da collect-uje myGCCol, instancu naše klase za koju smo sigurni da je na heap-u kao strong reference. Pošto collect propada (jer je instanca još "živa") GC odlučuje da je referenca verovatno tu da ostane i promoviše je u sledeću generaciju. Indirektno ako je Generation prešao iz 0 u 1 to znači da je GC bar jednom pokušao collect nad nultom generacijom. Zgodno za naše potrebe. Treba i napomenuti da je sistemski alocirana memorija na kraju prvog primera bila 6592kb, a na kraju drugog 7022kb (čak sam i stavljao ručno collect iza svakog load, pa je na kraju test11.exe imao 100kb sistemski alocirane memorije)
Elem:
1. Inicijalno je alocirano 24496 bajtova, tu su neke gluposti i naš myGCCol :)
2. Prvi Assembly.Load podiže alokaciju za oko 16kb (toliko je otprilike velik i sam included.dll), za sada je sve ok
3. Svako sledeće učitavanje inkrementalno podiže alokaciju za otprilike 8kb na svakih 10 učitavanja, odakle sledi:
3a. Assembly koji je već u memoriji ne učitava se dvaputa ako ne mora
3b. Assembly klasa za potrebe 3a verovano održava neku WeakReference tabelu čije održavanje dovodi do ovog minimalnog povećanja alocirane memorije
4. U prvom primeru posle 10 učitavanja generation je 0 (što znači da GC nije nijednom pokušao da počisti heap)
5. U drugom primeru posle 100000 učitavanja alocirano je nekih 300kb, ali je generation = 1, što znači da je bar jednom GC počistio samo nultu generaciju da napravi mesta, tako da nam tih 300kb ne znači mnogo
6. Ali ;), posle GC.Collect, kad se GCu naloži da počisti sve generacije (što se vidi iz povećanja generation vrednosti), alocirana memorija u oba slučaja pada na oko 30k, što znači:
6a. U memoriji više nema nijedne instance included.dll assembly-a, inače bi na kraju moralo biti alocirano bar 40k.
Dakle, ono što ti treba da uradiš je da nulluješ sve reference koje direktno i indirektno pokazuju na assembly i da pustiš GC da radi svoj posao. Ti možeš da ga nateraš da pokupi mrtve assembly-e iz memeorije sa GC.Collect, ali to može degradirati performase, pošto u trenutku kad pozoveš GC.Collect SVE žive instance bivaju promovisane u sledeću generaciju, pa bi dosta instanci koje bi kasnije bile mrtve i u generaciji 0 (dakle collectable sa Collect(0)) biće u generaciji 1 i biće izbačene mnogo kasnije (kad baš zagusti pa collect(0) ne može da pribavi dovoljno memorije pa GC mora da uradi i Collect(1)).
Inače se zbog ovakvog ponašanja skoro sve .NET aplikacije ponašaju kao da su bagovite i da imaju "leak" probleme. Jednom sam pratio windows service koji je 10 (deset) dana konstantno punio memoriju po malo, dok 10-og dana nije sa svojih 100 linija koda napunio 50mb memorije. Onda je nešto kvrcnulo u runtimeu i GC je odradio Collect(0) i program je oslobodio 90% alocirane memorije i spao na 5mb. Znači nikada ne znaš kad će GC da uradi Collect(0), ako imaš dovoljno memorije mogu i meseci da prođu :), ali isto tako čim zagusti i ponestane memorije ume i Collect(2) da odradi i da istrese sve mrtve instance (trenutno instance mogu da doguraju samo do druge generacije, svaka sledeća provera ostavlja ga tu gde je (u drugoj). Ako mene pitate, veoma dobro osmišljen sistem.
Sloba je za 12 godina promenio antropološki kod srpskog naroda. On je od jednog
naroda koji je bio veseo, pomalo površan, od jednog naroda koji je bio znatiželjan, koji
je voleo da vidi, da putuje, da upozna,
od naroda koji je bio kosmopolitski napravio narod koji je namršten, mrzovoljan,
sumnjicav, zaplašen, narod koji se stalno nešto žali, kome je stalno neko kriv - Z.Đinđić