Tutorial Java – #9 Managementul memoriei, Garbage Collector si memory leaks

Inca din primele zile ale programarii (prin limbajele masina – assembler), o problema importanta de performanta a fost si inca este, dimensiunea spatiului de memorie disponibil si modul de gestiune al acestuia. In ciuda faptului ca astazi tehnologia hardware ofera resurse mari de memorie virtuala, Random Access Memory, dezvoltatorii de software trebuie sa acorde atentie modului in care aplicatiile gestioneaza memoria, deoarece pot fi implementate solutii gresite, care vor bloca procesul sau sistemul de operare, deoarece nu mai este suficienta memorie virtuala. In alte conditii, este posibil sa sa existe chiar constrangeri hardware si spatiul maxim de memorie disponibila poate fi o specificatie strict tehnica de care se va tine seama la dezvoltarea aplicatiei.

Alte subiecte care fac parte din acest tutorial Java sunt accesibile prin intermediul Java Tutorial 6 – Cuprins.

Procesul de Garbage Collection (o traducere corecta ar fi eliberare a memoriei) este direct legat de modul de utilizare a zonei de memorie, Heap, deoarece aceasta este parteaa din RAM in care sistemul de operare ofera, intr-o maniera dinamica, spatiu de memorie suplimentara pentru procesele care necesita acest lucru la run-time. Pentru a obtine spatiu in Heap si pentru a stoca valori acolo, trebuie sa treci printr-un proces cu 2 etape:

  • realizarea unei cereri de spatiu de memorie si stocarea adresei intr-o referinta/pointer, in functie de limbajul de programare se poate utiliza malloc in C, new in C++ si, de asemenea, new in Java;
  • initializarea zonei cu valori; zona de memorie din Heap este accesata prin adresa din referinta/pointer;

Un alt concept legat de eliberarea memoriei, garbage collection, este memory leaks (pierderi de memorie) . Acestea sunt situatii in care reduci spatiul de memorie disponibil in Heap prin rezervarea de spatiu si neeliberarea acestuia dupa ce a fost utilizat. in C si C++ aceasta este o problema reala pentru ca este un scenariu ce poate fi reprodus foarte usor, mai ales in cazul in care nu exista o intelegere corecta a pointerilor.

Urmatoru exemplu in C++ (chiar daca nu aveti cunostinte de C++, exemplul este usor de citit daca se face presupunerea pointerul int * reprezinta acelasi lucru ca referinta Java int []):

void main()
{
	while(true)
		//generare a unui memory leak de 4000 bytes
		int* array = new int[1000];
}

Exemplul C++ precedent, executa o bucla infinita si in fiecare dintre iteratii se solicita spatiu (in Heap) pentru un vector cu 1000 valori intregi. Este evident ca vectorul nu este niciodata folosit si adresa acestuia din Heap se pierde in iteratia urmatoare. Acesta este un exemplu simplu care genereaza pierderi de memorie, memory leaks. Daca rulati exemplul si sa verificati procesul in Task Manager (pentru Windows), veti vedea ca  spatiul de memorie al acestui proces este intr-o continua crestere, pâna când veti obtine un avertisment cu privire la dimensiunea redusa a memoriei virtuale sau pana cand procesul este oprit de sistemul de operare (depinde de sistemul de operare).

Versiunea corecta a exemplu (fara pierderi de memorie) arata ca aceasta:

void main()
{
	while(true)
	{
		//generare a unui memory leak de 4000 bytes
		int* array = new int[1000];
		//eliberarea memoriei
		delete[] array;
	}
}

Acelasi exemplu, scris in Java arata ca acesta:

public class Main{
	public static void main(String[] args) {
            while(true)
            {
                int[] intArray = new int[1000];
                //spatiu eliberat de Garbage Collector
            }
	}
}

insa si acest exemplu va genera pierderi de memorie (memory leaks), dar nivelul de memorie folosit de proces ramâne la un nivel stabil si scazut. Motivul este ca procesul Java ruleaza in Java Virtual Machine care va elibera memoria neutilizata din Heap cu o rutina interna numita Garbage Collector.

Exemplul anterior este unul simplu. in ciuda faptului ca aveti un Garbage Collector, care are grija de proesul Java in situatii de genul asta, trebuie sa fiti atenti la gestiunea memoriei pentru ca exista in continuare posibilitatea (nu atât de usor ca in C sau C++) pentru a scrie aplicatii Java, care vor ramane fara memorie.

Deoarece Java este un limbaj de programare obiectual si pentru ca toate obiectele sunt stocate in Heap (amintiti-va ca acestea sunt create folosind new), putem spune ca JVM Garbage Collector gaseste imposibil de gasit obiecte de reclama si spatiu, sa revina la pool-ul disponibil de resurse. Un obiect care nu mai poate fi utilizat (este vorba de un me) este o zona din Heap care nu mai poate fi gasit folosind o referinta.

Cum se foloseste garbage collector-ul

Garbage collector-ul a evoluat atat de mult incat pentru Java 1.6 stiti sigur:

  • foloseste diferite metode pentru a gestiona referintele si adresele din Heap; metoda cea mai triviala utilizeaza un tabel pentru a contoriza cât de multe referinte sunt utilizate pentru a ajunge la o anumita zona de memorie din Heap; atunci când numarul de referinte ajunge la 0, zona de memorie este eliberata;
  • este o rutina complexa si foarte eficienta a JVM care nu va interfera cu performanta procesului Java;
  • nu va salveaza din orice situatie de de tip  OutOfMemoryException; amintiti-va ca orice obiect valid (este accesibil printr-o referinta existenta) nu este colectat;
  • puteti solicita garbage collector-ul pentru a elibera memoria in mod explicit prin apelul metodei System.gc ();
  • Java nu este recomandat sa interveniti peste garbage collector apeland metoda System.gc (), deoarece nu ai niciun fel de garantii privind modul in care acesta se va comporta;

Cum se pot genera obiecte pierdute in Heap sau memory leaks in Java

Pentru a genera obiecte ce nu pot fi accesate sau memory leaks si sa te bucuri de beneficiile de a avea un garbage collector trebuie sa pierzi sau sa elimini toate referintele pentru acel obiect.

1. Cea mai simpla cale este de a anula o referinta (de a o face null) :

public class Main{
	public static void main(String[] args)
        {
            int[] intArray = null;
            //rezerva spatiu in Heap
            intArray = new int[10000];
            //prelucrare date
            //...
            //referinta devine null
            //vectorul devine eligibil pentru GC
            intArray = null;
	}
}

in acest fel, obiectul devine eligibil pentru garbage collector, deoarece nu avem nici o alta referinta utilizate pentru a ajunge la acel obiect.

2. Reinitializare referintei va face obiectul anterior referit sa fie imposibil de gasit:

public class Main{
	public static void main(String[] args)
        {
            int[] intArray = null;
            //rezerva spatiu in Heap
            intArray = new int[10000];
            //prelucrare date
            //...
            //crearea a unui nou obiect si utilizarea referintei existente
            //vectorul anterior devine eligibil pentru GC
            intArray = new int[99];
	}

in acest fel, primul obiect devine eligibil pentru garbage collector, deoarece nu avem nici o alta referinta utilizata pentru a ajunge la obiect.

3. Izolarea unei referinte este un scenariu mai putin evident, deoarece la o prima vedere se poate spune ca obiect dvs. este inca valid (poate fi accesat printr-o referinta). Daca luam in considerare clasa urmatoare:

class Student{
    int age;         //instance variable
    int[] marks;     //instance variable

    public Student()
    {
        this.age = 0;
        marks = new int[10];
    }
}

puteti vedea ca fiecare obiect Student contine o referinta de vector ca o variabila instanta. Daca vom testa aceasta clasa

public class Main{
	public static void main(String[] args)
        {
            Student s = null;
            //request space in Heap
            s = new Student();
            //process your data
            //...
            //remove the reference
            s = null;
	}
}

este clar ca obiectul Student va fi colectate de catre GC. Dar, ce se va intâmpla cu vectorul de intregi din interiorul obiectului. Daca vom analiza Heap-ul, putem vedea ca obiectul vector este inca referit prin intermediul referintei marks.

Exemplu de Referinta Izolata

Exemplu de Referinta Izolata

Dar ce putem spune despre referinta marks? Deoarece, aceasta este parte a unui obiect ce nu mai poate fi accesat, referinta este considerata izolata si, in acest caz, obiectul referit este marcat pentru eliberarea memoriei.

Alte subiecte care fac parte din acest tutorial Java sunt accesibile prin intermediul Java Tutorial 6 – Cuprins.