Comprender la memoria en la JVM

Como ya sabe, la JVM ejecuta programas Java dentro de sí misma. Como cualquier máquina virtual, tiene su propio sistema de organización de la memoria.

El diseño de la memoria interna indica cómo funciona su aplicación Java. De esta forma se pueden identificar cuellos de botella en el funcionamiento de aplicaciones y algoritmos. Vamos a ver cómo funciona.

Comprender la memoria en la JVM

¡Importante! El modelo original de Java no era lo suficientemente bueno, por lo que se revisó en Java 1.5. Esta versión se usa hasta el día de hoy (Java 14+).

Pila de hilos

El modelo de memoria de Java utilizado internamente por la JVM divide la memoria en pilas y montones de subprocesos. Veamos el modelo de memoria de Java, lógicamente dividido en bloques:

Pila de hilos

Todos los subprocesos que se ejecutan en la JVM tienen su propia pila . La pila, a su vez, contiene información sobre qué métodos ha llamado el subproceso. Llamaré a esto la "pila de llamadas". La pila de llamadas se reanuda tan pronto como el subproceso ejecuta su código.

La pila del subproceso contiene todas las variables locales necesarias para ejecutar métodos en la pila del subproceso. Un hilo solo puede acceder a su propia pila. Las variables locales no son visibles para otros subprocesos, solo para el subproceso que las creó. En una situación en la que dos subprocesos ejecutan el mismo código, ambos crean sus propias variables locales. Así, cada hilo tiene su propia versión de cada variable local.

Todas las variables locales de tipos primitivos ( boolean , byte , short , char , int , long , float , double ) se almacenan completamente en la pila de subprocesos y no son visibles para otros subprocesos. Un subproceso puede pasar una copia de una variable primitiva a otro subproceso, pero no puede compartir una variable local primitiva.

Montón

El montón contiene todos los objetos creados en su aplicación, independientemente del subproceso que creó el objeto. Esto incluye envoltorios de tipos primitivos (por ejemplo, Byte , Integer , Long , etc.). No importa si el objeto se creó y se asignó a una variable local o se creó como una variable miembro de otro objeto, se almacena en el montón.

A continuación se muestra un diagrama que ilustra la pila de llamadas y las variables locales (se almacenan en pilas), así como los objetos (se almacenan en el montón):

Montón

En el caso de que la variable local sea de tipo primitivo, se almacena en la pila del hilo.

Una variable local también puede ser una referencia a un objeto. En este caso, la referencia (variable local) se almacena en la pila de subprocesos, pero el objeto en sí se almacena en el montón.

Un objeto contiene métodos, estos métodos contienen variables locales. Estas variables locales también se almacenan en la pila de subprocesos, incluso si el objeto que posee el método se almacena en el montón.

Las variables miembro de un objeto se almacenan en el montón junto con el propio objeto. Esto es cierto tanto cuando la variable miembro es de un tipo primitivo como cuando es una referencia a un objeto.

Las variables de clase estáticas también se almacenan en el montón junto con la definición de clase.

Interacción con objetos

Todos los subprocesos que tienen una referencia al objeto pueden acceder a los objetos en el montón. Si un subproceso tiene acceso a un objeto, entonces puede acceder a las variables del objeto. Si dos subprocesos llaman a un método en el mismo objeto al mismo tiempo, ambos tendrán acceso a las variables miembro del objeto, pero cada subproceso tendrá su propia copia de las variables locales.

Interacción con objetos (montón)

Dos hilos tienen un conjunto de variables locales.Variable local 2apunta a un objeto compartido en el montón (Objeto 3). Cada uno de los hilos tiene su propia copia de la variable local con su propia referencia. Sus referencias son variables locales y, por lo tanto, se almacenan en pilas de subprocesos. Sin embargo, dos referencias diferentes apuntan al mismo objeto en el montón.

Tenga en cuenta que el generalObjeto 3tiene enlaces aObjeto 2YObjeto 4como variables miembro (mostradas por flechas). A través de estos enlaces, dos hilos pueden accederObjeto 2YObjeto4.

El diagrama también muestra una variable local (variable local 1del método dos ). Cada copia contiene diferentes referencias que apuntan a dos objetos diferentes (Objeto 1YObjeto 5) y no el mismo. Teóricamente, ambos subprocesos pueden acceder a ambosObjeto 1, por lo que aObjeto 5si tienen referencias a ambos objetos. Pero en el diagrama anterior, cada hilo solo tiene una referencia a uno de los dos objetos.

Un ejemplo de interacción con objetos.

Veamos cómo podemos demostrar el trabajo en código:

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

El método run() llama al método one() y one() a su vez llama a two() .

El método one() declara una variable local primitiva (localOne) de tipo int y una variable local (localDos), que es una referencia a un objeto.

Cada subproceso que ejecuta el método one() creará su propia copialocalOneYlocalDosen tu pila. VariableslocalOneestarán completamente separados unos de otros, estando en la pila de cada hilo. Un subproceso no puede ver qué cambios hace otro subproceso en su copialocalOne.

Cada subproceso que ejecuta el método one() también crea su propia copialocalDos. Sin embargo, dos copias diferenteslocalDosterminar apuntando al mismo objeto en el montón. El hecho es quelocalDosapunta al objeto al que hace referencia la variable estáticainstancia. Solo hay una copia de una variable estática y esa copia se almacena en el montón.

Entonces ambas copiaslocalDosterminan apuntando a la misma instancia compartida . La instancia compartida también se almacena en el montón. CoincideObjeto 3en el diagrama de arriba.

Tenga en cuenta que la clase Shared también contiene dos variables miembro. Las propias variables miembro se almacenan en el montón junto con el objeto. Dos variables miembro apuntan a otros dos objetosEntero. Estos objetos enteros corresponden aObjeto 2YObjeto 4en el diagrama

También tenga en cuenta que el método two() crea una variable local llamadalocalOne. Esta variable local es una referencia a un objeto de tipo Integer . El método establece el enlace.localOnepara apuntar a una nueva instancia de Integer . El enlace se almacenará en su copia.localOnepara cada hilo. Se almacenarán dos instancias de Integer en el montón, y debido a que el método crea un nuevo objeto Integer cada vez que se ejecuta, los dos subprocesos que ejecutan este método crearán instancias de Integer separadas . CoincidenObjeto 1YObjeto 5en el diagrama de arriba.

Observe también las dos variables miembro de la clase Shared de tipo Integer , que es un tipo primitivo. Dado que estas variables son variables miembro, todavía se almacenan en el montón junto con el objeto. Solo las variables locales se almacenan en la pila de subprocesos.