Înțelegerea memoriei în JVM

După cum știți deja, JVM rulează programe Java în sine. Ca orice mașină virtuală, are propriul sistem de organizare a memoriei.

Aspectul memoriei interne indică modul în care funcționează aplicația dvs. Java. În acest fel, pot fi identificate blocajele în funcționarea aplicațiilor și a algoritmilor. Să vedem cum funcționează.

Înțelegerea memoriei în JVM

Important! Modelul Java original nu a fost suficient de bun, așa că a fost revizuit în Java 1.5. Această versiune este folosită până în prezent (Java 14+).

Stiva de fire

Modelul de memorie Java utilizat intern de JVM împarte memoria în stive de fire și heaps. Să ne uităm la modelul de memorie Java, împărțit logic în blocuri:

Stiva de fire

Toate firele care rulează în JVM au propria lor stivă . Stiva, la rândul său, deține informații despre metodele apelate de firul. Voi numi asta „stiva de apeluri”. Stiva de apeluri se reia de îndată ce firul de execuție își execută codul.

Stiva firului de execuție conține toate variabilele locale necesare pentru a executa metode pe stiva firului de execuție. Un thread poate accesa doar propria stivă. Variabilele locale nu sunt vizibile pentru alte fire, ci doar pentru firul care le-a creat. Într-o situație în care două fire execută același cod, ambele își creează propriile variabile locale. Astfel, fiecare thread are propria sa versiune a fiecărei variabile locale.

Toate variabilele locale de tipuri primitive ( boolean , byte , short , char , int , long , float , double ) sunt stocate în întregime pe stiva de fire și nu sunt vizibile pentru alte fire. Un fir de execuție poate transmite o copie a unei variabile primitive unui alt fir, dar nu poate partaja o variabilă locală primitivă.

Morman

Heap-ul conține toate obiectele create în aplicația dvs., indiferent de firul care a creat obiectul. Aceasta include pachete de tipuri primitive (de exemplu, Byte , Integer , Long și așa mai departe). Nu contează dacă obiectul a fost creat și atribuit unei variabile locale sau creat ca o variabilă membru a altui obiect, acesta este stocat în heap.

Mai jos este o diagramă care ilustrează stiva de apeluri și variabilele locale (sunt stocate pe stive), precum și obiecte (sunt stocate pe heap):

Morman

În cazul în care variabila locală este de tip primitiv, aceasta este stocată în stiva firului de execuție.

O variabilă locală poate fi, de asemenea, o referință la un obiect. În acest caz, referința (variabila locală) este stocată pe stiva de fire, dar obiectul în sine este stocat pe heap.

Un obiect conține metode, aceste metode conțin variabile locale. Aceste variabile locale sunt, de asemenea, stocate pe stiva de fire, chiar dacă obiectul care deține metoda este stocat pe heap.

Variabilele membre ale unui obiect sunt stocate pe heap împreună cu obiectul însuși. Acest lucru este adevărat atât când variabila membru este de tip primitiv, cât și când este o referință la obiect.

Variabilele de clasă statice sunt, de asemenea, stocate în heap împreună cu definiția clasei.

Interacțiunea cu obiectele

Obiectele din heap pot fi accesate de toate firele de execuție care au o referință la obiect. Dacă un fir are acces la un obiect, atunci poate accesa variabilele obiectului. Dacă două fire apelează o metodă pe același obiect în același timp, ambele vor avea acces la variabilele membre ale obiectului, dar fiecare thread va avea propria copie a variabilelor locale.

Interacțiunea cu obiectele (heap)

Două fire au un set de variabile locale.Variabila locală 2indică către un obiect partajat pe heap (Obiectul 3). Fiecare dintre fire are propria copie a variabilei locale cu propria sa referință. Referențele lor sunt variabile locale și, prin urmare, sunt stocate pe stive de fire. Cu toate acestea, două referințe diferite indică același obiect din heap.

Vă rugăm să rețineți că generalulObiectul 3are link-uri cătreObiectul 2ȘiObiectul 4ca variabile membre (indicate prin săgeți). Prin aceste linkuri se pot accesa două fireObiectul 2ȘiObiect4.

Diagrama arată, de asemenea, o variabilă locală (variabila locala 1din metoda Două ). Fiecare copie a acestuia conține referințe diferite care indică două obiecte diferite (Obiectul 1ȘiObiectul 5) și nu același. Teoretic, ambele fire le pot accesa pe ambeleObiectul 1, deci săObiectul 5dacă au referiri la ambele aceste obiecte. Dar în diagrama de mai sus, fiecare fir are doar o referință la unul dintre cele două obiecte.

Un exemplu de interacțiune cu obiecte

Să vedem cum putem demonstra munca în cod:

public class MySomeRunnable implements Runnable() {

    public void run() {
        one();
    }

    public void one() {
        int localOne = 1;

        Shared localTwo = Shared.instance;

        //... do something with local variables

        two();
    }

    public void two() {
        Integer localOne = 2;

        //... do something with local variables
    }
}
public class Shared {

    // store an instance of our object in a variable

    public static final Shared instance = new Shared();

    // member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);
}

Metoda run() apelează metoda one() și one() la rândul său apelează two() .

Metoda one() declară o variabilă locală primitivă (localOne) de tip int și o variabilă locală (localDoi), care este o referire la un obiect.

Fiecare fir care execută metoda one() își va crea propria copielocalOneȘilocalDoiîn teancul tău. VariabilelocalOnevor fi complet separate unul de altul, fiind pe teancul fiecărui fir. Un fir de execuție nu poate vedea ce modificări le aduce un alt fir în copierea salocalOne.

Fiecare fir care execută metoda one() își creează și propria copielocalDoi. Cu toate acestea, două exemplare diferitelocalDoisfârșesc prin a arăta către același obiect de pe grămadă. Adevărul este călocalDoiindică obiectul referit de variabila staticăinstanță. Există o singură copie a unei variabile statice și acea copie este stocată în heap.

Deci ambele exemplarelocalDoisfârșesc prin a indica aceeași instanță partajată . Instanța partajată este, de asemenea, stocată pe heap. Se potrivesteObiectul 3în diagrama de mai sus.

Rețineți că clasa Partajată conține și două variabile membre. Variabilele membre în sine sunt stocate pe heap împreună cu obiectul. Două variabile membre indică alte două obiecteÎntreg. Aceste obiecte întregi corespundObiectul 2ȘiObiectul 4pe diagramă.

De asemenea, rețineți că metoda two() creează o variabilă locală numitălocalOne. Această variabilă locală este o referință la un obiect de tip Integer . Metoda stabilește legăturalocalOnepentru a indica o nouă instanță Integer . Link-ul va fi stocat în copia salocalOnepentru fiecare fir. Două instanțe Integer vor fi stocate pe heap și, deoarece metoda creează un nou obiect Integer de fiecare dată când este executată, cele două fire de execuție care execută această metodă vor crea instanțe Integer separate . Se potrivescObiectul 1ȘiObiectul 5în diagrama de mai sus.

Observați și cele două variabile membre din clasa Shared de tip Integer , care este un tip primitiv. Deoarece aceste variabile sunt variabile membre, ele sunt încă stocate pe heap împreună cu obiectul. Doar variabilele locale sunt stocate pe stiva de fire.