A pesar de tener años programando este era uno de los
temas que no lograba comprender al cien por ciento, después de investigar un
poco entendí que lo desconocía incluso más de lo que pensaba, ya que aunque
realmente sea un tema muy "simple" y pareciera al principio no muy
crítico (debido quizás al Garbage Collector), resulta necesario conocerlo para
comprender como se comporta nuestro programa en memoria, y para conocer cómo
resolver conscientemente esos frustrantes errores de
"StackOverFlowException" y "HeapOverFlowException"
(java.lang.OutOfMemoryError en este caso), cabe mencionar que los temas
abarcados aquí están enfocados a Java, aunque también pueden aplicar a otros
lenguajes.
La pila y el montículo (The Stack and the Heap)
Primero que nada debemos conocer que existen dos espacios
en donde se ejecutan nuestras aplicaciones, uno de ellos es la pila (Stack), en
ella se guardan las variables locales y los métodos en ejecución, el otro es el
montículo (Heap), en el se guardan las variables de instancia y los objetos.
Cada Hilo (Thread) de la aplicación tiene su propia pila (Stack) pero todos
comparten el mismo montículo (Heap), las variables y métodos de la pila (Stack)
son eliminados al terminar su ejecución, las variables y objetos del montículo
(Heap) son eliminados al no tener ninguna referencia hacia ellos y a capricho
del Garbage Collector (al menos en Java y C#, aquí los gurús de C aplican el clásico
"Ha Ha" con voz del Nelson), primero nos enfocaremos en la pila
(Stack).
La pila (Stack)
Quizás una de las preguntas existenciales más difíciles
es ¿De cuánto espacio dispone la pila (Stack)?, y la respuesta es un rotundo
depende, incluso entre las mismas versiones de JVM (Java Virtual Machine) en
distintas maquinas depende, y su explicación y configuración da para otra
publicación más (insertar link cuando se tenga) por lo cual no trataremos aquí
a fondo ese tema y solo te diremos que depende de la JVM, el sistema operativo,
la configuración y el hardware, la siguiente pregunta a resolver sería ¿Cómo
funciona la pila?, lo cual explicaremos a grandes rasgos, pero primero
necesitamos un pequeño código para la explicación.
public class Program{static int sumNNumbers(int n){if (n == 1){return 1;}else{return n + sumNNumbers(n - 1);}}public static void main(String[] args){System.out.println("Sum of the first 5 numbers: " + sumNNumbers(5));}}
La pila (Stack) funciona como su homónima estructura de
datos pila, el último elemento en llegar es el primero en salir (Last In First
Out, LIFO), su comportamiento es parecido a una pila de objetos, por ejemplo
una pila de platos si queremos añadir un nuevo plato a la pila lo ponemos en la
cima, cuando queremos retirar un plato tomamos también el de encima.
Lo mismo sucede en la pila (Stack), y en cada llamada se
almacena lo siguiente:
·
La dirección a la que se debe retornar después
de la llamada.
·
Los datos pasados como parámetros de la llamada.
·
Las variables locales utilizadas por la función
llamada.
·
Los datos devueltos, si la función los devuelve.
Si ejecutamos el código arriba expuesto, a grandes rasgos
la pila se comportaría de esta manera:
·
El método main es puesto en la pila (Stack), con
la variable args como parámetro.
·
El método println es puesto en la pila (Stack),
con la dirección a la cual debe retornar después de su llamada y con su
parámetro que es un String.
·
El método sumNNumbers es puesto en la pila
(Stack), con la dirección a la cual debe retornar y con un entero como
parámetro.
·
El método sumNNumbers es llamado recursivamente,
puesto en la pila (Stack) n veces, con todos sus datos (dirección retorno,
parámetros).
·
Una vez que llega a su último llamado, este es
puesto en la pila (Stack), con la dirección a la cual debe retornar, un entero
como parámetro, y al terminar su ejecución su valor de retorno, después regresa
a su línea de retorno, devuelve el resultado y finalmente elimina sus variables
y parámetros de la pila (Stack).
·
Se insertan uno por uno los valores de retorno
del método sumNNumbers, y también se eliminan uno por uno sus datos de la pila
(Stack) según se entreguen resultados.
·
Se retorna a la función println imprime el
resultado en pantalla, una vez terminada su ejecución elimina sus datos de la
pila (Stack) y regresa el método main.
·
El método main termina su ejecución y elimina
sus datos del Stack.
Internamente su comportamiento es más complejo pero para
fines didácticos nos basta, la tercera pregunta sería ¿Por qué se genera un
StackOverFlowException? Esto es debido a que el espacio de la pila (Stack) es
limitado, y cuando algún método quiere insertar sus datos en la pila
(Stack) y no se tiene espacio suficiente
se genera esta excepción. Un ejemplo sería un método que se llama
recursivamente de manera infinita ó en C# una propiedad que se auto asigne.
El montículo (Heap)
Otra pregunta difícil es ¿De cuánto espacio dispone el
montículo (Heap)? Al igual que la pila (Stack) depende de la JVM, el sistema
operativo, la configuración y el hardware, y ¿Cómo funciona el montículo
(Heap)? Es un espacio donde se guardan variables de instancia y objetos
(recuerda que es Java), que en lenguajes como Java y C# no pueden ser
eliminados directamente sino con el Garbage Collector, y en lenguajes como C
deben ser eliminados explícitamente (free), en este caso nos enfocaremos en
Java y un poco en el Garbage Collector.
Después de este mini examen (espero haber aprobado) vamos
directo al tema que es responder completamente la última pregunta. Los datos
guardados en el montículo (Heap) son las variables de instancia y los objetos,
pero ¿Qué es una variable de instancia? Son simplemente las propiedades
expuestas de los objetos (instancias), para ejemplificarlo crearemos una clase
llamada Car
class Stereo{public void turnOn(){System.out.println("Really cool music!!!");}}public class Car{String carModel; // Instance Variableint carYear; // Instance VariableStereo carStereo; // Instance Variablepublic static void main(String[] args){Car car;car = new Car();car.carModel = "Sentra";car.carYear = 2005;car.carStereo = new Stereo();car.turnOnStereo();}void turnOnStereo(){this.carStereo.turnOn();}}
En este ejemplo las variables de instancia son carModel,
carYear y carStereo, quizás en estos momentos piensas ¡¡Oye carStereo es un
objeto también!! Pero carStereo no es un objeto sino una variable de
referencia, ¿Qué es una variable de referencia?... Se explicara
más adelante.
Volviendo al montículo (Heap), los objetos también son
guardados en el mismo, en el código de Car hemos creado dos objetos, un objeto
Car y un objeto Stereo y la instrucción que reservo la memoria, creo los
objetos y regreso las referencias de los objetos es new, y ¿Cómo eliminamos los
objetos? Eliminando todas las referencias hacia ese objeto, es decir eliminando
las variables de referencia ó apuntando estas a otros objetos, aunque esto no
los elimina los hace elegibles por el Garbage Collector quien finalmente es el
encargado de eliminarlos.
Por último ¿Por qué se genera un java.lang. OutOfMemoryError?
Al igual que la pila (Stack) el montículo (Heap) también es limitado, el JVM se
asegura a través del Garbage Collector de tener la mayor cantidad de memoria
disponible, cuando el Garbage Collector no puede recuperar suficiente memoria
para la creación de un nuevo objeto se generá este error.
Variables de Referencia (Reference Variables)
Las variables de referencia, son tipos de datos
especiales que guardan la referencia al objeto, el tipo de objeto y datos
acerca de como acceder a los atributos y métodos del mismo, aunque es muy común
en Java decir que los objetos se pasan por referencia en realidad no es
exactamente así, lo que realmente sucede es que se hace una copia de una
variable de referencia a otra, es decir se copian los bits de una variable a
otra y como ambas hacen referencia al mismo objeto pareciera que se ha pasado
el objeto por referencia, el código de abajo ejemplifica esta situación.
public class Cat{String catName;public static void main(String[] args){Cat cat = new Cat();cat.Name = "Meow";System.out.println(cat.Name); // Imprime MeowsetCatName(cat);System.out.println(cat.Name); // Imprime Meow}static void setCatName(Cat cat){cat = new Cat();cat.Name = "Presley";System.out.println(cat.Name); // Imprime Presley}}
En el método main se crea un objeto Cat que es
referenciado a través de la variable de referencia cat, y se le asigna el
nombre "Meow" y se imprime, después se pasa como parámetro esta
variable de referencia para el método estático setCatName, dentro de este se
crea un nuevo objeto y se asigna a la variable de referencia que había sido
pasada como parámetro, se le da el nombre "Presley" y se imprime, al
regresar al método principal y volver a imprimir se muestra la cadena Meow, ¿Por
qué no se imprimió Presley?, esto es debido a que al utilizar la instrucción
new se creó un nuevo objeto y se referencio en la variable de referencia pasada
como parámetro, a partir de ese momento las dos variables dejaron de
referenciar al mismo objeto por lo que los cambios realizados en SetCatName no
afectaron a la objeto original cat.