Pila vs Montículo

Cuando un programa se ejecuta, los datos deben almacenarse en memoria. El espacio que ocupan los datos y su ubicación de almacenamiento se determinan por el tipo de dato. Durante la ejecución del programa se usan dos zonas de memoria para guardar información: Pila (Stack) y Montículo (Heap).

Pila (Stack)

La pila es un arreglo continuo de memoria que sigue el principio de datos Último en entrar, primero en salir (LIFO). Almacena tres tipos de información:

  • Valores de variables de determinados tipos
  • Contexto actual de ejecución del programa
  • Parámetros que se envían a los métodos
La pila se asemeja a un tubo de monedas: solo se pueden agregar y extraer datos por un único extremo.

Las operaciones de memoria de la pila son gestionadas automáticamente por el sistema, el desarrollador no necesita manipularlas manualmente. Comprender la lógica interna de la pila simplifica el análisis del flujo de ejecución y la lectura de la documentación oficial de C#.

Características principales de la pila

  • Solo se pueden agregar y eliminar datos en el tope de la pila
  • Guardar datos en el tope: Push (apilar)
  • Extraer datos del tope: Pop (desapilar)

Propiedades de la memoria de pila

  • Velocidad muy alta de asignación y liberación de memoria, usa bloques contiguos de RAM
  • Ciclo de vida vinculado a la llamada de método: el marco de pila se destruye automáticamente al finalizar el método
  • Almacena: variables de tipo valor, parámetros de método, variables locales, contexto de ejecución

Para entender mejor el funcionamiento del montículo, veamos un ejemplo de código sencillo

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Lenguaje del código: JavaScript (javascript)

La lógica de este ejemplo es muy sencilla. Desde la primera línea de la función Main, punto de entrada del programa, creamos una variable entera a, luego la variable b en la segunda línea. La tercera línea invoca la función Add enviando los valores de a y b al interior del método. x recibe el valor de a, y el valor de b; la función devuelve el resultado de x + y igual a 30 al punto de llamada para asignarlo a sum. Cuando la función Add termine de ejecutarse, x e y se desapilan, sum toma el valor 30 y el programa finaliza al concluir Main. A continuación analizamos paso a paso todo el proceso de Push y Pop en la pila.

Flujo de ejecución del programa y proceso Push/Pop de la pila

Al crear la variable a, los tipos primitivos integrados se asignan directamente en la pila mediante una operación Push
Ejecutamos la segunda línea de código, creamos una segunda variable entera b con valor 20. Los datos apilados primero quedan en la base de la pila, los nuevos en el tope
En la tercera línea se ejecuta primero Add(a,b) a la derecha del signo igual para invocar la función. Los valores de a y b se copian en x e y, se trata de paso por valor, a y x son variables independientes sin relación mutua.
En este paso solo se calcula x + y y se devuelve el resultado, sin almacenamiento permanente en la pila. El valor 30 se envía al punto de llamada como si fuera int sum = 30;. Después de ejecutar return x+y, la función Add se prepara para finalizar, todas las variables locales del método se procesan con Pop, liberando el espacio de memoria que ocupaban para reutilizarlo posteriormente.
La posición superior de la pila se llama tope (Top); después de Pop el puntero del tope se desplaza hacia abajo.
Al finalizar Add, x e y son parámetros locales de esta función, se desapilan uno tras otro para liberar espacio y garantizar el funcionamiento fluido del sistema.
Tras el Pop, x e y desaparecen, el valor 30 devuelto por Add se envía al punto de llamada para guardarse en sum.
Al ejecutar int sum = 30 se produce otra operación Push, la variable sum con valor 30 se coloca en el tope de la pila. Al terminar esta línea, Main está listo para finalizar y las variables locales sum, b, a se destruirán secuencialmente con Pop.
Las operaciones Pop siempre se ejecutan desde el tope. La pila es como un pasillo con una única entrada y salida, todos los datos entran y salen por el mismo extremo. Por eso el orden de eliminación de variables en Main es inverso al de creación: primero sum, luego b, finalmente a.
La variable b se desapila y destruye, el puntero del tope de la pila se mueve automáticamente hacia abajo.
Todas las variables han sido desapiladas, incluida la última variable a. Estrictamente hablando, Main tiene otra variable local: el arreglo de cadenas args de Main(string[] args), que recibe los argumentos de línea de comandos al lanzar la consola. Los datos de este arreglo se guardan en dos zonas distintas de memoria, un tema relacionado con el montículo que omitimos en esta demostración para simplificar.

Recuerda un punto fundamental: durante la ejecución de una función las variables se crean de arriba abajo y se apilan en orden a → b → entrada en Add → x → y. Al terminar Add, x e y se desapilan, luego se apila sum; cuando Main finaliza, las variables se eliminan en orden inverso a su creación: sum, b, a.


Montículo (Heap)

El montículo es una amplia zona de memoria destinada a asignar bloques dinámicos para almacenar objetos de datos. A diferencia de la pila, los datos del montículo se pueden agregar y borrar en cualquier orden.

El programa no puede borrar manualmente los objetos del montículo; el Recolector de Basura (GC) del CLR (Common Language Runtime) limpia automáticamente los objetos huérfanos sin referencias inaccesibles. Esto evita errores derivados de la liberación manual de memoria obligatoria en otros lenguajes.

El montículo se puede comparar con un escritorio amplio donde se pueden colocar objetos libremente. Mientras que la pila vista antes es como un tubo de monedas con un único extremo de acceso.

El montículo es como un escritorio amplio sin restricciones de colocación. El desarrollador no tiene que borrar objetos del montículo manualmente, todo lo gestiona el GC. Si ninguna parte del programa hace referencia a un objeto, el GC lo destruye periódicamente para liberar memoria. Esto reduce mucho la carga del programador y evita caídas por fugas de memoria no liberadas. Lenguajes como C++ obligan al desarrollador a liberar toda la memoria del montículo de forma manual.

Flujo de recolección del GC

  1. El programa crea tres objetos en el montículo y mantiene referencias válidas hacia ellos
  2. El programa deja de usar uno de los objetos, todas sus referencias desaparecen
  3. El GC detecta el objeto huérfano sin referencias y libera la memoria que ocupa
  4. Recolección finalizada, la memoria liberada se puede asignar a nuevos objetos

Propiedades de la memoria del montículo

  • Espacios de memoria discontinuos, velocidad de asignación menor que la de la pila
  • Ciclo de vida no limitado por los límites de método, gestionado centralmente por el GC
  • Almacena: instancias de tipos de referencia (clases, arreglos, delegados, etc.); la pila solo guarda las direcciones de memoria de los objetos del montículo

Veamos un ejemplo práctico para clarificarlo. Como comentamos antes, las variables locales de tipo primitivo se guardan en la pila, mientras que los tipos definidos por el usuario suelen residir en el montículo. Revisa el código siguiente:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //Variables miembro
    private string name;

    private int age;

    //Constructor, se explicará más adelante
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //Método miembro
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Lenguaje del código: JavaScript (javascript)

La ilustración siguiente muestra la estructura de almacenamiento en memoria de las cuatro variables a, b, tom y lucy dentro de Main. Al igual que en la explicación de la pila, el código se ejecuta de arriba abajo: primero se apila a, luego b. Los valores de a y b se guardan directamente en la pila.

Las variables de tipo primitivo se almacenan directamente en la pila
Los datos reales del tipo personalizado Student se guardan en el montículo, en la pila solo se registra la dirección del bloque de memoria del objeto en el montículo.
Por ejemplo, los datos de tom están en el bloque 00001 del montículo, la pila guarda la dirección 00001 para localizar la información posteriormente.

Cuando la función Main termine su ejecución, la referencia de dirección lucy: 01010 en la pila se desapila y destruye. Solo se elimina la dirección, los datos reales del objeto en el montículo permanecen intactos. En ese momento el objeto del montículo pierde todas las referencias registradas por las variables de la pila.

El objeto Lucy del bloque 01010 del montículo ya no tiene ninguna variable en la pila que registre su dirección, se convierte en un objeto huérfano. El recolector de basura GC lo destruirá en el momento oportuno.
El objeto Lucy en el montículo pierde todas las referencias externas al desapilar la dirección almacenada en la pila. El GC limpiará la memoria de este objeto en cualquier instante para reutilizar el espacio. Mientras exista al menos una referencia al objeto en el programa, el GC no lo destruye.

Zonas de memoria

La pila y el montículo son divisiones lógicas de memoria a nivel de software, no particiones físicas del hardware.

Pila y montículo solo son separaciones lógicas definidas por el software; la memoria RAM física no cuenta con módulos hardware dedicados marcados como «zona de pila» o «zona de montículo».

La memoria RAM física solo está compuesta por chips uniformes con direcciones físicas consecutivas, no existe ningún marcador hardware que distinga la pila del montículo

El sistema operativo asigna un espacio de memoria virtual independiente a cada programa en ejecución y divide previamente esta memoria virtual en cuatro segmentos lógicos principales:

Segmento de código: Almacena las instrucciones compiladas del programa

Segmento estático global: Variables estáticas, constantes

Segmento de pila (Stack): El hilo principal y los hilos secundarios del programa disponen cada uno de un bloque de pila independiente

Segmento de montículo (Heap): Gran pool de memoria compartida para asignaciones dinámicas

La pila y el montículo que describimos aquí son únicamente zonas lógicas asignadas por el SO a cada proceso, sin aislamiento a nivel hardware entre ambos.

Pila vs Montículo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *