Mi-am propus ca in acest articol sa scriu despre modul in care .Net Framework, mai exact CLR, administreaza obiectele in memorie prin intermediul Garbage Collection. De asemenea, voi incerca sa identific legaturile dintre Finalize si Dispoze, folosite pentru crearea obiectelor de tip disposable.
Dupa cum stiti, ca programator C# nu trebuie sa eliberezi un obiect din memorie. Obiectele .Net sunt alocate in managed heap unde acestea vor fi distruse de garbage collector mai tarziu.
Managed heap
Managed heap este o “portiune de memorie” folosita de toate limbajele .Net pentru alocarea referintelor catre tipurile de obiecte create.
Sa definim o clasa.
/// <summary>
/// definitia pentru clasa Persoana
/// </summary>
public class Persoana
{
public string Nume { get; set; }
public string Prenume { get; set; }
public override string ToString()
{
return string.Format("{0} {1}", Nume, Prenume);
}
public Persoana(string nume, string prenume)
{
Nume = nume;
Prenume = prenume;
}
public int CalculeazaVarsta()
{
return 25;
}
}
Cream un obiect folosind cuvantul cheie new. Ce se intampla, de fapt? Cuvantul cheie new va returna o referinta catre acest obiect din managed heap.
//cream un obiect in memoria heap
//va fi returnata o referinta catre acest obiect
Persoana p = new Persoana("Nume", "Prenume");
In C#, se aloca unei clase o instanta prin folosirea cuvantului cheie new (operator, in acest caz) si apoi, pur si simplu, programatorul poate uita de ea. Obiectul va fi distrus de Garbage Collector. Evident, intrebarea este Cand?.
Sa presupunem ca la crearea unui obiect nou, nu exista destula memorie pentru alocare.
In acest moment incepe procesul de colectare.
Garbage collector
Un obiect va fi distrus de Garbage Collector atunci cand respectivul obiect nu mai este folosit.
Cand garbage collection porneste, se va uita intr-un set de referinte numit GC roots (locatii de memorie care contin referinte catre obiectele din managed heap.
Application roots sunt de mai multe categorii. De exemplu:
- referinte catre variabile globale.
- referinte catre campuri/obiecte statice.
- referinte catre obiecte care urmeaza sa fie finalizate.
In timpul procesului garbage collection, CLR va verifica obiectele din managed heap pentru a determina daca pot fi accesate din aplicatie. Pentru acest lucru, CLR va construi un object graph, care contine fiecare obiect de pe heap.
Cand CLR va incerca sa localizeze obiectele care nu sunt accesibile din aplicatie, bineinteles ca nu va lua fiecare obiect in parte. Va dura mult prea mult timp, mai ales in cazul in unor aplicatii mari. Pentru optimizare, fiecare obiect din heap este atribuit unei generatii.
Ideea de la baza folosirii generatiilor este urmatoarea: cu cat un obiect exista de mai mult timp in memoria heap, cu atat este mai mare probabilitatea ca el sa ramana acolo. De exemplu, pentru o forma a unui program desktop, clasa care defineste forma va fi tinuta in memorie pana la inchiderea programului. Spre deosebire de aceasta clasa, un obiect alocat intr-o metoda va fi clasificat ca “unreachable” mai repede.
Dupa identificarea obiectelor “garbage” si compactarea obiectelor care nu vor intra in garbage collection, se reincearca crearea obiectului nou. De aceasta data va fi cu succes.
Sa presupunem ca avem o metoda care ne instantiaza clasa definita.
private void CreeazaPersoana()
{
Persoana p = new Persoana("Nume","Prenume");
}
Observatie:
Referinta primita la crearea obiectului p nu am transmis-o in afara functiei.
Atunci cand se va incheia apelul metodei, referinta nu va mai fi gasita si obiectul asociat poate fi distrus de Garbage Collector. Totusi, nu se poate garanta fapul ca acest lucru se va intampla imediat. Dar cand se va intampla, va fi distrus in siguranta.
Garbage Collection in versiunile .Net
De la versiunea de .Net Framework 1.0 pana la 3.5, CLR curata obiectele nefolosite printr-o tehnica numita concurrent garbage collection.
In versiunea 4.0, noua modalitate se numeste background garbage collection si se scrie ca aduce imbunatiri.
Aceste doua notiuni nu fac parte din tema acestui articol si vor fi tratate in viitor.
System.Gc
Pentru a programa cu Garbage Collector, in BCL se gaseste clasa Gc.
Cu ajutorul ei, putem forta inceperea garbage collection-ului.
GC.Collect();
Un moment in care se poate folosi este cel dupa alocarea unui numar foarte mare de obiecte si se doreste repede eliberarea memoriei. Sau atunci cand se vrea ca executarea codului curent sa nu fie intrerupta de un eventual “garbage collection”.
Mai multe detalii despre acesta clasa puteti gasi pe msdn.
Se recomanda ca programatorii sa interactioneze cat mai putin cu System.Gc.
Finalize () – apel implicit
Toti programatorii .Net stiu ca System.Object, clasa de baza, detine o metoda virtuala, Finalize.
Implementarea metodei Finalize se mai numeste finalizer. Un finalizer va elibera doar resurse externe, continute de obiectul respectiv. Implementarile finalizer sunt apelate de GC atunci cand obiectul nu mai este folosit (cand spre el nici un alt obiect nu mai are referinta).
Pentru suprascrierea metodei Finalize nu se foloseste clasicul override.
protected override void Finalize()
{
}
Pentru codul de mai sus, vom primi mesajul de eroare la compilare Do not override object.Finalize. Instead, provide a destructor.
Procedura corecta consta in definirea unui destructor:
/// <summary>
/// destructor
/// </summary>
~Persoana ()
{
//cod pentru eliberarea resurselor din memorie
}
In cazul unui struct este incorect sa suprascriem Finalize pentru ca o structura este de tip valoare, iar tipurile valoare nu sunt alocate in heap si nu vor fi luate in considerare de garbage collecter.
Apelul pentru metoda Finalize va avea loc in momentul in care GC se apeleaza sau atunci cand se forteaza apelarea lui.
Majoritatea claselor pe care un programator le defineste nu vor avea nevoie de o eliberare explicita pentru ca vor fi colectate de GC.
Rolul metodei Finalize este sa se asigure ca un obiect poate elibera resursele unmanaged atunci cand este colectat de GC. Cu exceptia acestui caz, nu este indicat ca tipul definit de noi sa suporte Finalize pentru simplu motiv ca procesul de garbage collection va necesita mai mult timp. CLR determina automat daca un obiect suporta metoda Finalize la alocarea lui in memoria heap. Daca da, obiectul este marcat ca finalizable si un pointer catre acest obiect va fi adaugat intr-o coada finalization queue. Aceasta contine toate obiectele ce trebuie finalizate inainte de scoaterea lor din memoria heap. Cand GC intervine, el va examina fiecare inregistrare din finalization queue si va copia obiectul de pe heap intr-o alta structura “managed”, numita finalization reachable table. In acest moment, pe un alt thread va fi invocata metoda Finalize pentru fiecare obiect din finalization reachable table.
Datorita faptului ca programatorul nu poate stabili momentul cand GC apeleaza Finalize, folosirea destructorilor trebuie sa fie ca un mecanism de “back-up” pentru eliberarea resursele unmanaged. Modul recomandat este cel pe care il vom analiza in sectiunea urmatoare, implementarea interfetei IDisposable.
IDisposable – apel explicit
O alternativa, o sa vedeti ca e mai mai mult o completare, la suprascrierea metodei Finalize este implementarea interfetei IDisposable si apelarea singurei metode definite in aceasta interfata, Dispose.
/// <summary>
/// definitia pentru clasa Persoana care implementeaza interfata IDisposable
/// </summary>
public class Persoana : IDisposable
{
public string Nume { get; set; }
public string Prenume { get; set; }
public override string ToString()
{
return string.Format("{0} {1}", Nume, Prenume);
}
public Persoana(string nume, string prenume)
{
Nume = nume;
Prenume = prenume;
}
public int CalculeazaVarsta()
{
return 25;
}
public void Dispose()
{
//cod pentru eliberarea resurselor
Console.WriteLine("Disposed");
}
}
Spre deosebire de Finalize, care putea fi folosita doar pentru clase, IDisposable poate fi utilizata si pentru structuri.
Daca un obiect suporta IDisposable, atunci se recomanda apelarea metodei Dispose.
Este o practica des intalnita ca atunci cand folosim obiecte managed care implementeaza IDisposable , sa tratam posibilele exceptii.
try
{
p.CalculeazaVarsta();
}
finally
{
p.Dispose();
}
Pentru a scapa de acest “ambalare” intr-o constructie try/finally se poate folosi using.
using (Persoana p = new Persoana("Nume","Persoana"))
{
p.CalculeazaVarsta();
}
Exista un articol pe msdn, Implementing Finalize and Dispose to Clean Up Unmanaged Resources in care se prezinta un pattern, un model, pentru implementarea IDisposable si Finalize intr-un mod performant.
Pe scurt, modul cum functioneaza GC:
- cauta obiectele mangaged care au referinta in cod.
- incearca sa finalizeze obiectele care nu mai au referinta in cod.
- elibereaza obiectele care nu mai au referinta in cod si “recupereaza” memoria alocata acestora.
Principalul motiv pentru interactiunea cu GC il reprezinta crearea unor clase care vor opera asupra resurselor interne, unmanaged.
Daca veti cauta pe internet despre GC, veti observa ca este una din cele mai controversate tehnologii din .Net.
A programa intr-un mediu cu garbage collected simplifica mult modul de dezvoltare. De exemplu, in C++, programatorii trebuie sa stearga manual obiectele alocate, iar memory leak-urile trebuie detectate. Pasand permisiunea garbage collector-ului sa distruga obiectele, de managementul memoriei nu se mai ocupa programatorul, ci CLR.