CodeGym /Cursos /JAVA 25 SELF /Estructura de la memoria en la JVM: pila, heap, PermGen/M...

Estructura de la memoria en la JVM: pila, heap, PermGen/MetaSpace

JAVA 25 SELF
Nivel 64 , Lección 0
Disponible

1. Visión general de la memoria de un proceso Java

Cuando ejecutas un programa Java, la JVM (Java Virtual Machine) le pide al sistema operativo un trozo de memoria. A veces pequeño, a veces bastante grande (especialmente si ejecutas, por ejemplo, algún Minecraft con un montón de mods). Esa memoria se divide en varias áreas clave, cada una de las cuales cumple su papel:

  • Pila (stack) — para variables locales y llamadas a métodos.
  • Heap — para todos los objetos que creas con new.
  • Áreas de servicio (PermGen/MetaSpace) — para metadatos de clases, campos estáticos y otras «cosas mágicas».

Se ve aproximadamente así:

┌───────────────────────────────┐
│ Proceso de la JVM             │
│ ┌─────────────┐               │
│ │  Stack      │ ← ¡Cada hilo — su propia pila!
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Compartido por todos los hilos
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ PermGen/      │ ← Metadatos de clases
│ │ MetaSpace     │
│ └───────────────┘             │
└───────────────────────────────┘

¿Por qué es importante?

  • Entender la estructura de la memoria ayuda a escribir código más eficiente y seguro.
  • Es más fácil diagnosticar errores como StackOverflowError o OutOfMemoryError.
  • No asustan expresiones como «recolector de basura» y «fuga de memoria»: entiendes dónde y qué buscar.

2. Pila (stack): rápida, local, pero no para siempre

La pila es un área especial de memoria que se asigna para cada hilo por separado. La pila se parece a una pila de platos: el último que pones es el primero que sacarás, pero solo después de retirar los anteriores. Es decir, la pila funciona con el principio LIFO (Last In, First Out).

¿Para qué sirve la pila?

En la pila se almacenan:

  • Variables locales de los métodos (por ejemplo, int x = 5; dentro de un método).
  • La dirección de retorno tras la llamada a un método (para saber adónde volver cuando el método termina).

Cada vez que llamas a un método, en la pila se añade un nuevo frame (stack frame), algo así como una caja que contiene todas las variables locales de ese método e información de servicio. Cuando el método termina, su frame se elimina — todas sus variables locales desaparecen.

Ejemplo

public static void main(String[] args) {
    int a = 10;           // a está en la pila de main
    int b = sum(a, 5);    // llamamos a sum
}

public static int sum(int x, int y) {
    int result = x + y;   // x, y y result están en la pila de sum
    return result;
}
  • Cuando se llama a sum, se crea para él un frame independiente en la pila.
  • Tras finalizar sum, sus variables desaparecen.

Ciclo de vida de una variable

Las variables locales viven solo mientras se ejecuta el método en el que están declaradas. En cuanto el método termina — ya no existen; la memoria se libera al instante.

Desbordamiento de pila

Si por accidente (o a propósito) escribes una recursión infinita, cada llamada de método añadirá un nuevo frame a la pila. En algún momento la pila se agotará y obtendrás:

Exception in thread "main" java.lang.StackOverflowError

Ejemplo:

public static void main(String[] args) {
    recurse();
}
public static void recurse() {
    recurse(); // ¡Recursión infinita!
}

Tamaño de la pila

El tamaño de la pila es limitado — normalmente son unos pocos megabytes por hilo (puede establecerse con el parámetro -Xss). Si la pila se agota — el programa falla con un error.

3. Heap: el lugar para tus objetos

El heap es un área de memoria compartida por todos los hilos, donde viven todos los objetos que creas con new, así como los arrays. Es en el heap donde ocurre toda la «magia» de la programación orientada a objetos.

¿Cómo llegan los objetos al heap?

String s = new String("Hello");
int[] arr = new int[10];
  • La variable s es una referencia; está en la pila.
  • El objeto String y el array arr — están en el heap.

Ciclo de vida de un objeto

Un objeto vive en el heap mientras exista al menos una referencia fuerte (strong reference) hacia él. En cuanto nadie se refiere al objeto — se convierte en «basura» y puede ser eliminado por el recolector de basura (GC).

Gestión de memoria

A diferencia de C/C++, donde tú mismo debes ocuparte de liberar la memoria (free, delete), en Java de eso se encarga el GC. No puedes liberar un objeto explícitamente, pero puedes poner a null todas las referencias a él — entonces será candidato a eliminación.

Esquema: ¿dónde está cada cosa?

Stack (main)
  └─ s ─┬────────────┐
         │           │
         ▼           │
      Heap           │
   ┌─────────────┐   │
   │ String "Hello"◄──┘
   └─────────────┘

Particularidades del heap

  • El heap es único para todo el proceso de la JVM.
  • El tamaño del heap puede establecerse al arrancar (-Xmx, -Xms).
  • Si ya no queda espacio libre en el heap y el GC no puede liberar memoria — el programa falla con OutOfMemoryError.

4. PermGen y MetaSpace: ¿dónde viven las clases?

Cuando escribes class MyClass { ... } y luego ejecutas el programa, la JVM debe almacenar en alguna parte todo lo relacionado con esa clase — métodos, campos, bytecode, variables estáticas, constantes e incluso literales de cadena. Para eso existe en la JVM un área especial de memoria donde «viven» las clases.

Antes, hasta Java 8, esta área se llamaba PermGen (Permanent Generation). Pero tenía no pocos problemas — por ejemplo, tenía un tamaño fijo y, si no había espacio suficiente, la aplicación simplemente se caía con el error OutOfMemoryError: PermGen space.

Con la llegada de Java 8 apareció un área nueva y más flexible — MetaSpace. Sustituyó a la antigua PermGen y ahora puede ampliarse automáticamente, ocupando tanta memoria como necesite el sistema (dentro de la disponible físicamente).

PermGen (hasta Java 8)

  • En PermGen se almacenaban metadatos de clases, campos estáticos y literales de cadena.
  • El tamaño de PermGen estaba limitado (por defecto pequeño); podía aumentarse con el parámetro -XX:MaxPermSize=256m.
  • Si una aplicación cargaba dinámicamente muchas clases (por ejemplo, en servidores web), PermGen podía «agotarse» y obtenías un error:
java.lang.OutOfMemoryError: PermGen space
  • Problema: la limpieza de PermGen no siempre ocurría correctamente si las clases se descargaban dinámicamente (por ejemplo, al reiniciar aplicaciones web).

MetaSpace (Java 8+)

  • Desde Java 8, PermGen desapareció y apareció MetaSpace.
  • MetaSpace almacena metadatos de clases, pero ahora en memoria nativa (fuera del heap de Java).
  • El tamaño de MetaSpace por defecto no está limitado (solo lo limita la memoria del sistema), pero puede establecerse un límite con -XX:MaxMetaspaceSize=512m.
  • El error por falta de memoria ahora se ve así:
java.lang.OutOfMemoryError: Metaspace
  • En MetaSpace también se almacenan campos estáticos, métodos e información sobre las clases.

Esquema: cómo está organizado todo

┌───────────────────────────────┐
│ Proceso de la JVM             │
│ ┌─────────────┐               │
│ │  Stack      │ ← Variables locales, llamadas a métodos
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← Objetos, arrays, todo lo que va con new
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ MetaSpace     │ ← Metadatos de clases, campos estáticos
│ └───────────────┘             │
└───────────────────────────────┘

¿Por qué es importante?

Si escribes aplicaciones de escritorio o de servidor normales, lo más probable es que nunca te encuentres con errores de PermGen o MetaSpace. Pero si trabajas con carga dinámica de clases (por ejemplo, plugins, aplicaciones web, frameworks como Spring que pueden cargar y descargar un montón de clases), entonces conocer MetaSpace es un must-have.

5. Ilustración: diagrama de memoria de la JVM

flowchart TD
    subgraph JVM
        direction TB
        Stack1["Pila (hilo 1)"]
        Stack2["Pila (hilo 2)"]
        Heap[Heap]
        MetaSpace[MetaSpace]
    end
    Stack1 --hace referencia a--> Heap
    Stack2 --hace referencia a--> Heap
    Heap --usa clases de--> MetaSpace
  • Cada hilo — su propia pila.
  • Todas las pilas pueden referirse a objetos en el heap.
  • Los objetos en el heap «conocen» su clase, cuya información está en MetaSpace.

6. Ejemplo: cómo se ve en código real

public class MemoryDemo {
    public static void main(String[] args) {
        int x = 42; // x está en la pila de main
        String s = "Hello!"; // s — referencia en la pila; el objeto String en el heap; el literal "Hello!" en MetaSpace
        Person p = new Person("Alice"); // p — referencia en la pila; el objeto Person en el heap

        // Llamemos al método para crear un nuevo frame de pila
        printPerson(p);
    }

    public static void printPerson(Person person) {
        // person — referencia en la pila de printPerson
        System.out.println(person.getName());
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() { return name; }
}

Análisis:

  • x — variable local; vive en la pila del método main.
  • s — referencia en la pila; el objeto String en el heap; y el literal de cadena "Hello!" — en MetaSpace.
  • p — referencia en la pila; el objeto Person en el heap.
  • La clase Person y todos sus métodos/campos — en MetaSpace (metadatos de clase).
  • La llamada printPerson(p) crea un nuevo frame de pila; dentro de él la referencia local person apunta al mismo objeto en el heap.

7. Cómo gestiona la JVM la memoria: FAQ breve

¿Puedo gestionar la pila?
No; la pila está completamente bajo el control de la JVM. Solo puedes establecer su tamaño al arrancar (-Xss).

¿Puedo gestionar el heap?
Parcialmente: el tamaño del heap se establece al arrancar (-Xmx, -Xms). De la limpieza se encarga el recolector de basura (GC).

¿Puedo gestionar MetaSpace?
Puedes limitar su tamaño (-XX:MaxMetaspaceSize), pero normalmente no es necesario.

¿Qué ocurre cuando falta memoria?
— Si se agota la pila — StackOverflowError.
— Si se agota el heap — OutOfMemoryError: Java heap space.
— Si se agota MetaSpace — OutOfMemoryError: Metaspace.

8. Errores típicos al trabajar con memoria

Error n.º 1: StackOverflowError por recursión infinita. La causa más frecuente — olvidar la condición de salida de la recursión. Por ejemplo, un método se llama a sí mismo sin parar. La JVM no puede ampliar la pila hasta el infinito y el programa «se caerá».

Error n.º 2: OutOfMemoryError por desbordamiento del heap. Si creas demasiados objetos a los que siguen refiriéndose variables/colecciones (por ejemplo, añades elementos a una lista y nunca los eliminas), el heap puede agotarse.

Error n.º 3: OutOfMemoryError: PermGen space / Metaspace. Si usas plugins o cargas dinámicamente un montón de clases y MetaSpace no se limpia (por ejemplo, por una descarga incorrecta de clases), puede agotarse el espacio en MetaSpace.

Error n.º 4: confundir la referencia con el objeto. Muchos principiantes se confunden: una variable de tipo Person en la pila — es solo una referencia, mientras que el objeto está en el heap.

Error n.º 5: esperar que el recolector de basura lo elimine todo al instante. El GC trabaja «cuando le toca» (en realidad — según algoritmos internos y cuando falta memoria), y no inmediatamente después de que pongas a null una referencia. No conviene confiar en una liberación instantánea de memoria.

1
Tarea
JAVA 25 SELF, nivel 64, lección 0
Bloqueada
StackOverflowError – Cascada traviesa de recursión 🧙‍♂️
StackOverflowError – Cascada traviesa de recursión 🧙‍♂️
1
Tarea
JAVA 25 SELF, nivel 64, lección 0
Bloqueada
OutOfMemoryError – El Devorador Insaciable de Memoria 💾
OutOfMemoryError – El Devorador Insaciable de Memoria 💾
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION