Saltearse al contenido

Stack y Heap para la gestión de memoria

El stack y el heap son áreas de memoria clave que la Java Virtual Machine (JVM) utiliza para almacenar y administrar datos durante la ejecución de un programa en Java. Si bien los conceptos generales del stack y el heap se aplican a la mayoría de los lenguajes de programación, hay ciertos detalles específicos de cómo Java maneja estas áreas que vale la pena revisar.

El Stack

El stack es un área de memoria que sigue un esquema de pila Last Input First Output (LIFO). Cuando se invoca un nuevo método en Java, se crea un nuevo marco (frame) en la parte superior del stack. Este marco contiene:

  • Parámetros y variables locales del método
  • Información para restaurar el estado del procesador al final del método
  • Referencia al marco del método que lo invocó (caller frame)

Cuando se completa el método, el marco se descarta de la parte superior del stack. Esto garantiza que los recursos usados por un método se liberen de manera eficiente.

Es importante tener en cuenta el stack solo almacena datos primitivos y referencias a objetos. No almacena los objetos en sí, sino referencias a los objetos en el heap. El espacio en el stack es limitado y se asigna de manera eficiente, por lo que es más rápido acceder a los datos almacenados en el stack que en el heap.

Configurar el Tamaño del Stack

La configuración del tamaño del stack es importante para optimizar el rendimiento de una aplicación Java. Si el tamaño del stack es demasiado pequeño, puede provocar un StackOverflowError. Si es demasiado grande, puede consumir más memoria de la necesaria. En Java, podemos ajustar el tamaño durante la creación de los hilos usando la opción -Xss de la JVM. Por ejemplo:

Ventana de terminal
java -Xss<tamaño> MiProgramaJava

Ahora, reemplazamos <tamaño> con el tamaño deseado del stack en kilobytes. Por ejemplo, -Xss1m establece el tamaño del stack en 1 MB, o -Xss256k en 256 KB.

Ventana de terminal
java -Xss1m MiProgramaJava

Es importante tener en cuenta que la memoria total usada por el stack se calcula multiplicando el tamaño del stack por el número de hilos en ejecución. Por lo tanto, si tenemos muchos hilos en nuestra aplicación, debemos tener en cuenta el tamaño total del stack para evitar problemas de memoria.

StackOverflowError

Un StackOverflowError ocurre cuando un método Java crea demasiados marcos en el stack, generalmente debido a recursión excesiva no intencional o a un diseño deficiente. Cuando el stack está desbordado (overflow), el JVM simplemente se detiene.

Características del Stack

  • El stack accede a los marcos de pila (stack frames) a través de desplazamientos fijos en lugar de depender de la desreferenciación de punteros. Esto mejora la velocidad de acceso.
  • Se almacena en la memoria RAM de la computadora. El tamaño de la memoria del stack suele ser mucho menor que el espacio de memoria disponible en el heap. Normalmente entre 512 KB y 1 MB por hilo.
  • Las variables almacenadas en el stack tienen un tamaño fijo predeterminado que no cambia en tiempo de ejecución.
  • Los stack frames se almacenan y acceden en orden LIFO (Last In, First Out). El último frame en entrar es el primero en salir.
  • Cada hilo en una aplicación Java tiene su propia pila de ejecución. Esto permite que los hilos sean independientes y no interfieran entre sí.
  • La Máquina Virtual de Java (JVM) asigna y libera automáticamente la memoria del stack. No es necesario liberar la memoria manualmente.
  • Contiene variables locales, llamadas a métodos y referencias a objetos del heap. Solo almacena referencias a objetos, no los objetos en sí.
  • Las variables locales y los parámetros de un método se almacenan en el stack. Cuando el método se completa, se liberan automáticamente.

El Heap

El heap es un área de memoria usada para la asignación dinámica de objetos y arrays en tiempo de ejecución. A diferencia del stack, el heap se administra de manera automática y su tamaño se puede ajustar dinámicamente mientras se ejecuta el programa.

Cuando se crea una nueva instancia de objeto mediante el operador new, se reserva espacio en el heap para ese objeto y sus miembros. El objeto recibe una dirección de memoria única, y una referencia a esa dirección se guarda en el stack o dentro de otro objeto en el heap.

El heap está dividido en regiones llamadas generaciones: joven (young) y antigua (old). Los objetos recién creados se colocan en la generación joven. Cuando la generación joven se llena, se activa un recolector de basura menor que mueve los objetos aún en uso a la generación antigua y libera el espacio utilizado por los objetos no referenciados.

OutOfMemoryError

Un OutOfMemoryError ocurre cuando el heap se queda sin espacio para asignar nuevos objetos. Esto puede deberse a una fuga de memoria (memory leak) o a la creación de demasiados objetos grandes. En cualquier caso, el JVM no puede asignar más memoria y lanza una excepción OutOfMemoryError.

Tamaño del Heap

Los objetos de Java a los que se hace referencia permanecen activos en el heap durante su vida útil y ocupan memoria. Estos objetos son accesibles globalmente desde cualquier parte de la aplicación. Cuando los objetos ya no tienen referencias, son elegibles para la recolección de basura y liberar la memoria ocupada del heap.

El tamaño del heap de Java está determinado por dos atributos de la JVM, que se pueden configurar al iniciar la aplicación:

  • Xms para establecer el tamaño inicial del heap
  • Xmx para establecer el tamaño máximo del heap

Ejemplo de Stack y Heap

Ahora que entendemos los conceptos de stack y heap, veamos un ejemplo simple en Java que ilustra cómo se almacenan los datos en cada área de memoria. El código no está optimizado y se ha simplificado para facilitar la comprensión.

1
class Empleado {
11 collapsed lines
2
String nombre;
3
Integer sueldo;
4
Integer ventas;
5
Integer bono;
6
7
public Empleado(String nombre, Integer sueldo, Integer ventas, Integer bono) {
8
this.nombre = nombre;
9
this.sueldo = sueldo;
10
this.ventas = ventas;
11
this.bono = bono;
12
}
13
}
14
15
public class Prueba {
16
static int PORCENTAJE_BONO = 10;
17
18
static int calcularBono(int sueldo) {
19
int bono = sueldo * PORCENTAJE_BONO / 100;
20
return bono;
21
}
22
23
static int calcularBonos(Empleado[] empleados) {
24
int totalBonos = 0;
25
for (Empleado empleado : empleados) {
26
empleado.bono = calcularBono(empleado.sueldo);
27
totalBonos += empleado.bono;
28
}
29
return totalBonos;
30
}
31
32
public static void main(String[] args) {
33
Empleado[] empleados = new Empleado[2];
34
empleados[0] = new Empleado("Juan", 1000, 100, 0);
35
empleados[1] = new Empleado("Pedro", 2000, 200, 0);
36
37
int totalBonos = calcularBonos(empleados);
38
System.out.println("Total de bonos: " + totalBonos);
39
}
40
}