Comprendre la mémoire dans la JVM

Comme vous le savez déjà, la JVM exécute des programmes Java en elle-même. Comme toute machine virtuelle, elle possède son propre système d'organisation de la mémoire.

La disposition de la mémoire interne indique le fonctionnement de votre application Java. De cette manière, les goulots d'étranglement dans le fonctionnement des applications et des algorithmes peuvent être identifiés. Voyons voir comment ça fonctionne.

Comprendre la mémoire dans la JVM

Important! Le modèle Java d'origine n'était pas assez bon, il a donc été révisé dans Java 1.5. Cette version est utilisée à ce jour (Java 14+).

Pile de fils

Le modèle de mémoire Java utilisé en interne par la JVM divise la mémoire en piles de threads et en tas. Regardons le modèle de mémoire Java, logiquement divisé en blocs :

Pile de fils

Tous les threads exécutés dans la JVM ont leur propre pile . La pile, à son tour, contient des informations sur les méthodes appelées par le thread. J'appellerai cela la "pile d'appels". La pile d'appels reprend dès que le thread exécute son code.

La pile du thread contient toutes les variables locales requises pour exécuter des méthodes sur la pile du thread. Un thread ne peut accéder qu'à sa propre pile. Les variables locales ne sont pas visibles par les autres threads, uniquement par le thread qui les a créées. Dans une situation où deux threads exécutent le même code, ils créent tous les deux leurs propres variables locales. Ainsi, chaque thread a sa propre version de chaque variable locale.

Toutes les variables locales de types primitifs ( boolean , byte , short , char , int , long , float , double ) sont entièrement stockées sur la pile de threads et ne sont pas visibles par les autres threads. Un thread peut transmettre une copie d'une variable primitive à un autre thread, mais ne peut pas partager une variable locale primitive.

Tas

Le tas contient tous les objets créés dans votre application, quel que soit le thread qui a créé l'objet. Cela inclut les wrappers de types primitifs (par exemple, Byte , Integer , Long , etc.). Peu importe si l'objet a été créé et affecté à une variable locale ou créé en tant que variable membre d'un autre objet, il est stocké sur le tas.

Ci-dessous un diagramme qui illustre la pile d'appels et les variables locales (elles sont stockées sur des piles) ainsi que des objets (elles sont stockées sur le tas) :

Tas

Dans le cas où la variable locale est de type primitif, elle est stockée sur la pile du thread.

Une variable locale peut également être une référence à un objet. Dans ce cas, la référence (variable locale) est stockée sur la pile de threads, mais l'objet lui-même est stocké sur le tas.

Un objet contient des méthodes, ces méthodes contiennent des variables locales. Ces variables locales sont également stockées sur la pile de threads, même si l'objet propriétaire de la méthode est stocké sur le tas.

Les variables membres d'un objet sont stockées sur le tas avec l'objet lui-même. Cela est vrai à la fois lorsque la variable membre est de type primitif et lorsqu'il s'agit d'une référence d'objet.

Les variables de classe statiques sont également stockées sur le tas avec la définition de classe.

Interaction avec les objets

Les objets sur le tas sont accessibles par tous les threads qui ont une référence à l'objet. Si un thread a accès à un objet, il peut accéder aux variables de l'objet. Si deux threads appellent une méthode sur le même objet en même temps, ils auront tous deux accès aux variables membres de l'objet, mais chaque thread aura sa propre copie des variables locales.

Interaction avec les objets (tas)

Deux threads ont un ensemble de variables locales.Variable locale 2pointe vers un objet partagé sur le tas (Objet 3). Chacun des threads a sa propre copie de la variable locale avec sa propre référence. Leurs références sont des variables locales et sont donc stockées sur des piles de threads. Cependant, deux références différentes pointent vers le même objet sur le tas.

Veuillez noter que le généralObjet 3a des liens versObjet 2EtObjet 4en tant que variables membres (indiquées par des flèches). Grâce à ces liens, deux threads peuvent accéderObjet 2EtObjet4.

Le diagramme montre également une variable locale (variable locale 1de methodTwo ). Chaque copie contient des références différentes qui pointent vers deux objets différents (Objet 1EtObjet 5) et pas le même. Théoriquement, les deux threads peuvent accéder aux deuxObjet 1, de manière àObjet 5s'ils ont des références à ces deux objets. Mais dans le diagramme ci-dessus, chaque thread n'a qu'une référence à l'un des deux objets.

Un exemple d'interaction avec des objets

Voyons comment nous pouvons démontrer le travail dans le code :

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

La méthode run() appelle la méthode one() , et one() appelle à son tour two() .

La méthode one() déclare une variable locale primitive (localOne) de type int et une variable locale (localDeux), qui est une référence à un objet.

Chaque thread exécutant la méthode one() créera sa propre copielocalOneEtlocalDeuxdans votre pile. variableslocalOneseront complètement séparés les uns des autres, étant sur la pile de chaque thread. Un thread ne peut pas voir les changements qu'un autre thread apporte à sa copielocalOne.

Chaque thread exécutant la méthode one() crée également sa propre copielocalDeux. Cependant, deux exemplaires différentslocalDeuxfinissent par pointer vers le même objet sur le tas. Le fait est quelocalDeuxpointe vers l'objet référencé par la variable statiqueexemple. Il n'y a qu'une seule copie d'une variable statique, et cette copie est stockée sur le tas.

Donc les deux exemplaireslocalDeuxfinissent par pointer vers la même instance partagée . L' instance partagée est également stockée sur le tas. Ça correspondObjet 3dans le schéma ci-dessus.

Notez que la classe Shared contient également deux variables membres. Les variables membres elles-mêmes sont stockées sur le tas avec l'objet. Deux variables membres pointent vers deux autres objetsEntier. Ces objets entiers correspondent àObjet 2EtObjet 4sur le schéma.

Notez également que la méthode two() crée une variable locale nomméelocalOne. Cette variable locale est une référence à un objet de type Integer . La méthode établit le lienlocalOnepour pointer vers une nouvelle instance Integer . Le lien sera stocké dans sa copielocalOnepour chaque fil. Deux instances Integer seront stockées sur le tas, et comme la méthode crée un nouvel objet Integer à chaque exécution, les deux threads exécutant cette méthode créeront des instances Integer séparées . Ils correspondentObjet 1EtObjet 5dans le schéma ci-dessus.

Notez également les deux variables membres de la classe Shared de type Integer , qui est un type primitif. Étant donné que ces variables sont des variables membres, elles sont toujours stockées sur le tas avec l'objet. Seules les variables locales sont stockées sur la pile de threads.