Comprensione della memoria nella JVM

Come già sai, la JVM esegue programmi Java al suo interno. Come ogni macchina virtuale, ha il proprio sistema di organizzazione della memoria.

Il layout della memoria interna indica come funziona l'applicazione Java. In questo modo è possibile identificare i colli di bottiglia nel funzionamento di applicazioni e algoritmi. Vediamo come funziona.

Comprensione della memoria nella JVM

Importante! Il modello Java originale non era abbastanza buono, quindi è stato rivisto in Java 1.5. Questa versione è utilizzata fino ad oggi (Java 14+).

Pila di fili

Il modello di memoria Java utilizzato internamente dalla JVM divide la memoria in stack di thread e heap. Diamo un'occhiata al modello di memoria Java, suddiviso logicamente in blocchi:

Pila di fili

Tutti i thread in esecuzione nella JVM hanno il proprio stack . Lo stack, a sua volta, contiene informazioni su quali metodi ha chiamato il thread. Lo chiamerò "stack di chiamate". Lo stack di chiamate riprende non appena il thread esegue il suo codice.

Lo stack del thread contiene tutte le variabili locali necessarie per eseguire i metodi nello stack del thread. Un thread può accedere solo al proprio stack. Le variabili locali non sono visibili agli altri thread, solo al thread che le ha create. In una situazione in cui due thread eseguono lo stesso codice, entrambi creano le proprie variabili locali. Pertanto, ogni thread ha la propria versione di ciascuna variabile locale.

Tutte le variabili locali di tipi primitivi ( boolean , byte , short , char , int , long , float , double ) sono archiviate interamente nello stack del thread e non sono visibili ad altri thread. Un thread può passare una copia di una variabile primitiva a un altro thread, ma non può condividere una variabile locale primitiva.

Mucchio

L'heap contiene tutti gli oggetti creati nell'applicazione, indipendentemente dal thread che ha creato l'oggetto. Ciò include wrapper di tipi primitivi (ad esempio, Byte , Integer , Long e così via). Non importa se l'oggetto è stato creato e assegnato a una variabile locale o creato come variabile membro di un altro oggetto, viene archiviato nell'heap.

Di seguito è riportato un diagramma che illustra lo stack di chiamate e le variabili locali (sono archiviate negli stack) nonché gli oggetti (sono archiviati nell'heap):

Mucchio

Nel caso in cui la variabile locale sia di tipo primitivo, viene memorizzata nello stack del thread.

Una variabile locale può anche essere un riferimento a un oggetto. In questo caso, il riferimento (variabile locale) viene archiviato nello stack di thread, ma l'oggetto stesso viene archiviato nell'heap.

Un oggetto contiene metodi, questi metodi contengono variabili locali. Queste variabili locali vengono archiviate anche nello stack di thread, anche se l'oggetto che possiede il metodo è archiviato nell'heap.

Le variabili membro di un oggetto vengono archiviate nell'heap insieme all'oggetto stesso. Questo è vero sia quando la variabile membro è di tipo primitivo sia quando è un riferimento a un oggetto.

Anche le variabili di classe statica vengono archiviate nell'heap insieme alla definizione della classe.

Interazione con gli oggetti

È possibile accedere agli oggetti nell'heap da tutti i thread che hanno un riferimento all'oggetto. Se un thread ha accesso a un oggetto, allora può accedere alle variabili dell'oggetto. Se due thread chiamano contemporaneamente un metodo sullo stesso oggetto, entrambi avranno accesso alle variabili membro dell'oggetto, ma ogni thread avrà la propria copia delle variabili locali.

Interazione con oggetti (heap)

Due thread hanno un insieme di variabili locali.Variabile locale 2punta a un oggetto condiviso nell'heap (Oggetto 3). Ognuno dei thread ha la propria copia della variabile locale con il proprio riferimento. I loro riferimenti sono variabili locali e sono quindi memorizzati su stack di thread. Tuttavia, due riferimenti diversi puntano allo stesso oggetto nell'heap.

Si prega di notare che il generaleOggetto 3ha collegamenti aOggetto 2EOggetto 4come variabili membro (indicate dalle frecce). Attraverso questi link possono accedere due threadOggetto 2EOggetto4.

Il diagramma mostra anche una variabile locale (variabile locale 1da methodTwo ). Ogni sua copia contiene riferimenti diversi che puntano a due oggetti diversi (Oggetto 1EOggetto 5) e non lo stesso. Teoricamente, entrambi i thread possono accedere a entrambiOggetto 1, così aOggetto 5se hanno riferimenti a entrambi questi oggetti. Ma nel diagramma sopra, ogni thread ha solo un riferimento a uno dei due oggetti.

Un esempio di interazione con gli oggetti

Vediamo come possiamo dimostrare il lavoro nel codice:

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);
}

Il metodo run() chiama il metodo one() e one() a sua volta chiama two() .

Il metodo one() dichiara una variabile locale primitiva (localOne) di tipo int e una variabile locale (localTwo), che è un riferimento a un oggetto.

Ogni thread che esegue il metodo one() creerà la propria copialocalOneElocalTwonella tua pila. VariabililocalOnesaranno completamente separati l'uno dall'altro, trovandosi nello stack di ciascun thread. Un thread non può vedere quali modifiche apportate da un altro thread alla sua copialocalOne.

Ogni thread che esegue il metodo one() crea anche la propria copialocalTwo. Tuttavia, due copie diverselocalTwofiniscono per puntare allo stesso oggetto nell'heap. Il fatto è chelocalTwopunta all'oggetto a cui fa riferimento la variabile staticaesempio. Esiste solo una copia di una variabile statica e tale copia è archiviata nell'heap.

Quindi entrambe le copielocalTwofiniscono per puntare alla stessa istanza condivisa . Anche l' istanza condivisa viene archiviata nell'heap. CorrispondeOggetto 3nel diagramma sopra.

Si noti che la classe Shared contiene anche due variabili membro. Le stesse variabili membro vengono memorizzate nell'heap insieme all'oggetto. Due variabili membro puntano ad altri due oggettiNumero intero. Questi oggetti interi corrispondono aOggetto 2EOggetto 4sul diagramma.

Si noti inoltre che il metodo two() crea una variabile locale denominatalocalOne. Questa variabile locale è un riferimento a un oggetto di tipo Integer . Il metodo imposta il collegamentolocalOneper puntare a una nuova istanza Integer . Il collegamento verrà memorizzato nella sua copialocalOneper ogni filo. Due istanze Integer verranno archiviate nell'heap e poiché il metodo crea un nuovo oggetto Integer ogni volta che viene eseguito, i due thread che eseguono questo metodo creeranno istanze Integer separate . CorrispondonoOggetto 1EOggetto 5nel diagramma sopra.

Notare anche le due variabili membro nella classe Shared di tipo Integer , che è un tipo primitivo. Poiché queste variabili sono variabili membro, vengono comunque archiviate nell'heap insieme all'oggetto. Solo le variabili locali vengono memorizzate nello stack di thread.