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.