La pila, el montículo y las variables de referencia (The stack, the heap and the reference variables)

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 Variable
                int carYear; // Instance Variable
                Stereo carStereo; // Instance Variable
               
                public 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 Meow
                               setCatName(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.

1 comentario:

  1. Y yo que pensaba que StackOverFlow era donde preguntabas cosas de programación... :P Saludos y excelente post!

    ResponderEliminar