El stack y el heap son dos áreas de memoria con funciones distintas durante la ejecución de un programa Java.
Hasta ahora, hemos creado variables como int a = 5 o String s = "Hola". Hemos asumido que Java “guarda” esos datos en la memoria RAM y ya está.
Pero, ¿dónde los guarda exactamente? ¿Y cómo?
Si modificas un objeto mediante una variable y observas el cambio desde otra, ambas variables probablemente contienen referencias al mismo objeto.
Vamos a abrir la caja negra de la memoria en Java con un modelo simplificado que resulta útil para razonar sobre variables, objetos y llamadas a métodos.
Las dos zonas de memoria: stack y heap
La JVM utiliza varias áreas de memoria. Para empezar nos centraremos en dos: el stack de cada hilo, donde se gestionan las llamadas a métodos, y el heap compartido, donde normalmente se alojan los objetos.
Es la zona de memoria de ejecución inmediata.
- Ordenada: Funciona como una pila de platos (LIFO: Last In, First Out).
- Ligada a las llamadas: Cada invocación crea un frame con su estado local.
- Efímera: Cuando un método termina, su frame deja de estar disponible.
- Pequeña: Tiene un tamaño limitado (si te pasas, ocurre el famoso
StackOverflowError).
Conceptualmente, aquí se guarda el estado local de las llamadas. La JVM puede optimizar algunos valores y llevarlos a registros, así que no conviene interpretar este modelo como un mapa físico exacto.
Es la zona compartida donde normalmente se crean los objetos y arrays.
- Dinámica: Los objetos se crean aquí y se quedan hasta que nadie los usa.
- Gestionada: Aquí es donde trabaja el Garbage Collector (Recolector de Basura) para limpiar lo que no sirve.
Como modelo mental, los objetos creados con new viven en el heap. La JVM puede aplicar optimizaciones internas siempre que no cambie el comportamiento observable del programa.
Tipos por valor vs tipos por referencia
Aquí viene el concepto clave. Dependiendo del tipo de dato, Java lo guarda de forma distinta.
Tipos primitivos (por valor)
Los 8 tipos primitivos que vimos (int, double, boolean, char…) son Tipos por Valor.
- Comportamiento: La variable contiene el valor primitivo. Si es una variable local forma parte de su frame; si es un campo, forma parte del objeto que la contiene.
Si tienes int a = 10, en la memoria Stack hay una cajita llamada a que tiene dentro el número 10.
int a = 10;
int b = a; // Se hace una FOTOCOPIA del valor 10.
b = 20; // Cambiamos la copia.
System.out.println(a); // Imprime 10. El original NO cambia.Esto es intuitivo. Al copiar, clonamos el dato.
Tipos objeto (por referencia)
Cualquier cosa que no sea un primitivo (Clases, Arrays, Strings, Scanner) es un Tipo por Referencia.
- El objeto vive normalmente en: El heap.
- La variable contiene: Una referencia. Una variable local forma parte del frame; un campo de referencia forma parte de su objeto.
La variable no contiene el objeto, sino una referencia que permite localizarlo. Java no expone esa referencia como una dirección de memoria manipulable.
Veamos el código peligroso:
// Imagina una clase Punto con x, y
Punto p1 = new Punto(0, 0);
Punto p2 = p1; // ¡OJO! Copiamos la REFERENCIA (el mando), no el objeto.
p2.x = 50; // Usamos el mando 2 para cambiar el canal de la tele.
System.out.println(p1.x); // Imprime 50. ¡p1 ha cambiado!Como p1 y p2 apuntan al mismo objeto en el Heap, lo que hagas con uno afecta al otro. Esto se llama Aliasing.
El valor null
Ahora que entendemos las referencias, entender null es fácil.
null significa que tienes un mando a distancia (variable en el Stack) que no apunta a ninguna televisión (ningún objeto en el Heap).
Si intentas pulsar un botón en ese mando (variable.metodo()), Java no encuentra la tele y explota: NullPointerException.
Paso de parámetros: ¿por valor o referencia?
Esta es una pregunta de entrevista técnica típica. En Java, el paso de parámetros SIEMPRE es por valor.
- Si pasas un
int, se pasa una copia del número. - Si pasas un Objeto, se pasa una copia de la referencia (una copia del mando a distancia).
public void cambiar(int n, Punto p) {
n = 99; // Modifica la copia local de n. El n original no cambia.
p.x = 99; // Modifica el objeto al que apunta el mando p. El objeto original SÍ cambia.
p = null; // Rompe mi mando local. El mando original sigue apuntando al objeto.
}Es sutil, pero importante: puedes modificar el contenido del objeto que te pasan, pero no puedes hacer que la variable original apunte a otro objeto.