Estructura de la memoria de Objetos Java

Un poco de teoría bajo nivel sobre el funcionamiento de Java y como maneja la memoria. Esto es un repaso sencillo de cómo Java maneja la memoria, cuanta memoria utiliza un objeto y como mejorar la utilización de memoria de nuestros objetos.

Shallow Copy Size vs Deep Size

Shallow Copy Size es la cantidad de memoria ocupada por un sólo POJO al crearlo. Deep Size por otra lado, es la cantidad de memoria que ocupa un POJO sumado a la memoria utilizada por todos los POJOs que referencia.

Los 8 bytes

Todos los objetos en Java deben tener un tamaño que sea múltiplo de 8 bytes, es decir, una granularidad de 8 bytes. La razón de esto es la facilidad de los distintos sistemas operativos y procesadores de convertir direcciones de memoria a registros de cuatro en cuatro (o de ocho en ocho para sistemas de 64bits). Esto es así para todas las plataformas a diferencia de C o C++ que utilizan distintos tamaños de memoria para tipos primitivos. 

Todos los objetos -menos los arreglos ( String [] array )- tienen una cabecera de dos direcciones, donde la primera es un código Hash de identificación y la segunda contiene una referencia a la clase del objeto. De esta forma, al instanciar un nuevo objeto Object() se utilizarán 8 bytes de la memoria heap (no entremos en las demás posibilidades ahora) para mantener el uid y la referencia ya que Object() no tiene ningún campo o variable.

Despues de estos 8 bytes de la cabecera, siguen los bytes necesarios para los atributos de la clase, los cuales siempre son alineados en la memoria según su tamaño:

- double y long
- float e int
- char y short
- boolean y byte

Reestructuración de nuestras clases.

La JVM es capaz de ordenar los atributos de nuestras clases de tal manera que los nuevos objetos no ocupen más memoria de la que necesitan.

Class DespilfarradorDeMemoria {
    byte a;
    int b;
    boolean c,
    long d;
    Object e;
}

Si la JVM no fuera capaz de reordenar los atributos de esta clase al instanciarla, nuestro nuevo objeto desperdiciaría unos 14 bytes de memoria:


Se puede ver en la imagen como se va reservando memoria para mantener al objeto dentro de un múltiplo de 8 bytes por lo que se desperdicia mucha memoria. Para esto la JVM es capaz de ordenar los atributos de tal manera que sólo se utilice la cantidad de memoria necesaria y se reduzca al mínimo la consumida para relleno. La imagen del objeto instanciado sería la siguiente:


Gracias a esto, se reduce el consumo innecesario a 6 bytes y en total 32 para dicho objeto.

Calcular el consumo de memoria

Ahora podemos calcular el consumo de memoria de cualquier objeto que extiende de Object() directamente. Probemos con un boolean:

Por increíble que parezca, una instancia de un objeto boolean consume ¡16 bytes! De estos 16, 7 son usados como relleno para mantener la granularidad. Este es el coste de que todo en Java sea un objeto.

Las mezclas son malas

Whisky, cerveza y tequila: malo. Mezclar atributos entre clases de la hierarquía: muy malo. La primera regla es: primero los atributos de las clases padres seguidas por los atributos de las clases instanciadas.


La imagen describe la asignación de memoria para los objetos "Hija":

class Padre{
    long a;
    int b;
    int c;
}

class Hija extends Padre{
    long d;
}

La segunda regla es que se utilizará un relleno entre las asignaciones de dos objetos para separarlos y lograr la granuralidad de 4 bytes:

class Padre{
    byte a;
}

class Hija extends Padre{
    byte c;
}

Como se puede ver, se utiliza un relleno de 3 bytes para obtener 12 bytes, los cuales no pueden ser utilizados para los atributos del objeto siguiente.

La tercera y ultima regla viene a establecerse para casos concretos en los que la segunda regla no puede ser aplicada, como cuando el primer elemento de la clase hija es un "long" o un "double" y la clase padre no termina con un limite de 8 bytes.

class Padre{
    byte a;
}

class Hija extends Padre{
    long b;
    short c;
    byte d;
}


Cuando el primer campo de una sub-clase es un double o un long y la super-clase no se alinea a los 8 bytes, la JVM romperá la segunda regla e intentará colocar los campos int, short, byte y referencias al principio del espacio reservado para la sub-clase hasta que se rellene por completo.


En la imagen se puede ver que en el byte 12 cuando la clase Padre ha terminado, se comienza con un short de 2 bytes y un byte de 1 byte antes del long para ahorrar 3 o 4 bytes que de otra forma se habrían desperdiciado:

Como siempre tenemos otro caso especial con las clases internas no estáticas, la cuales tienen un campo escondido que mantiene la referencia a la clase a la que pertenece. Esta es una referencia regular que sigue los mismos principios anteriores. Por esto, las clases interntas tienen un coste extra de 4 bytes.