En general la memoria de una computadora se puede clasificar en:
- Memoria primaria (o central)
- Constituida por memoria volátil, más rápida y costosa que otros medios de almacenamiento. Es la memoria de trabajo del procesador, la RAM (Random Access Memory) de acceso rápido, donde se encuentran los programas en ejecución y sus datos. La información que almacena la RAM se pierde al interrumpirse el suministro eléctrico.
- Memoria secundaria
- Conformada por el conjunto de memorias no volátiles. Es la memoria de persistencia de información. Ejemplos: discos rígidos, discos ópticos, memorias USB, etc. Conserva la información almacenada al interrumpirse el suministro de corriente eléctrica. Es más lenta y barata que la RAM.
1Organización de la memoria primaria¶
Tanto los programas como el resto de la información que guarda la computadora se encuentran almacenados en la memoria secundaria.
Al lanzar la ejecución de un programa, las instrucciones y los datos iniciales se copian a la memoria primaria. Un programa en ejecución se denomina proceso.
En tiempo de ejecución, un proceso está asociado a una porción de la memoria primaria que se divide lógicamente en 4 segmentos:
- Segmento de código (code segment)
- Es la porción donde se localizan las instrucciones que componen nuestro programa. Su tamaño se determina al comenzar la ejecución. Asociado a este segmento se encuentra un puntero que indica la próxima instrucción a ejecutar.
- Segmento de datos (data segment)
- Almacena las variables globales y estáticas. Su tamaño también queda determinado al comenzar la ejecución.
- Pila (stack segment)
- Almacenará el contenido de las variables locales en cada invocación de una función. Cada entrada en la pila constituye un marco de datos (stack frame) que representa el contexto de una función en ejecución e incluye variables locales, parámetros y valores de retorno. Se asigna como un bloque de memoria contigua.
- En Go, el tamaño inicial del stack es pequeño (generalmente 2KB) pero no es de tamaño fijo como en C, sino que tiene tamaño dinámico (el runtime lo agranda si hace falta).
- Memoria dinámica (heap)
- Es el espacio de memoria que se utiliza para la asignación dinámica de memoria. Su tamaño no está determinado al comenzar la ejecución y se va ajustando a medida que el programa solicita más memoria para almacenar datos. No se asigna como un único bloque contiguo, sino que puede fragmentarse.
2Ejecución de programas¶
Estos son algunos de los aspectos a tener en cuenta al momento de ejecutar un programa:
- 1. Compilación y almacenamiento.
- Los programas se almacenan inicialmente en la memoria secundaria (por ejemplo, en un disco duro o SSD). En el caso de Go como se trata de un lenguaje compilado, los archivos fuentes se compilan y se genera un archivo ejecutable que se almacena en el disco.
- 2. Asignación de memoria central
- Al iniciar la ejecución, el sistema operativo carga el programa en la memoria central (RAM). Esto incluye las instrucciones del programa y los datos iniciales necesarios para su ejecución. La memoria asignada al programa se divide en segmentos específicos para el código, los datos, el stack y el heap. El programa en ejecución se denomina proceso.
- 3. Ejecución del programa
- El procesador ejecuta una a una las instrucciones del programa desde el segmento de código. Cada vez que ejecuta una instrucción, avanza el puntero de instrucción a la siguiente instrucción. Cuando ejecuta una llamada a una función, se crea un nuevo frame en el stack para almacenar las variables locales y los parámetros de la función. Al terminar la función, el stack frame se elimina y el valor de retorno se transfiere al marco anterior desde donde se llamó a la función. Si durante la ejecución de una función se solicita memoria dinámica, se asigna en el heap.
- En Go, durante la ejecución del programa, el recolector de basura (garbage collector) se encarga de liberar la memoria no utilizada en el heap, evitando así las fugas de memoria (memory leaks).
- 4. Interacción con el sistema operativo
- El sistema operativo supervisa y gestiona la memoria asignada al programa. Si el programa necesita más memoria, puede solicitarla al sistema operativo, que ajustará el tamaño del heap o el stack según sea necesario.
- 5. Liberación de memoria
- Al finalizar la ejecución, el sistema operativo libera toda la memoria asignada al programa, incluyendo los segmentos de código, datos, pila y heap.
En la siguiente figura se muestra un esquema de la memoria de un proceso en ejecución. Cada segmento de memoria tiene un tamaño y una función específica en el programa. La figura es solo a modo didáctico y no representa la organización real de la memoria en Go, que es más compleja.
En el diagrama el stack se ubica en la parte superior de la memoria y crece hacia abajo; cuando no puede crecer más se produce un error de desbordamiento de pila (stack overflow). El heap se ubica en la parte inferior de la memoria, sobre los segmentos de código y datos y crece hacia arriba.
Segmentos de memoria de un proceso en ejecución
Segmentos de memoria de un proceso en ejecución
3Gestión de memoria dinámica en Go¶
La gestión de memoria es un aspecto clave en cualquier lenguaje de programación, ya que impacta en el rendimiento, la eficiencia y la estabilidad del software.
En Go, las variables se almacenan en el stack o en el heap dependiendo de su alcance, duración y cómo se utilizan.
El compilador de Go decide automáticamente si una variable debe almacenarse en el stack o en el heap. Esto se conoce como Escape Analysis. Si una variable “escapa” del alcance de la función, se almacena en el heap en lugar del stack.
Veamos un ejemplo, dado el siguiente fragmento de código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26type Direccion struct { calle, ciudad, provincia string numero uint } type Persona struct { nombre, apellido string edad uint direccion Direccion } var num int = 5 var p1 = Persona{"Marcelo", "Díaz", 27, Direccion{"Mariano Acosta", "Gonzalez Catán", "Buenos Aires", 6420}} func main() { p2 := Persona{nombre: "Pepe", edad: 23} p3 := &p2 p4 := Persona{"Juan", "Gonzalez", 34, Direccion{"Valentín Gómez", "Caseros", "Buenos Aires", 742}} p3.direccion := Direccion{"Av. Corrientes", "CABA", "Buenos Aires", 1050} p3.apellido = "Martinez" p4.direccion = Direccion{"Mariano Acosta", "Gonzalez Catán", "Buenos Aires", 6420} }
El Stack, el Heap y el Segmento de Datos presentarán el siguiente estado:
Mapa de Memoria
Mapa de Memoria
Vamos a analizar cada variable en el código y justificar dónde se almacena:
num1var num int = 5Almacenamiento: Segmento de Datos
Justificación:
numes una variable global (declarada a nivel de paquete). Al no ser una variable local, no se ubica en el stack de ninguna función, sino en el segmento de datos estáticos definido al inicio del proceso.
p11 2var p1 = Persona{"Marcelo", "Díaz", 27, Direccion{"Mariano Acosta", "Gonzalez Catán", "Buenos Aires", 6420}}Almacenamiento: Segmento de Datos (estructura) y Heap (cadenas)
Justificación:
p1es una variable global, que se encuentra en el segmento de datos, es de tipoPersonay ocupa un espacio de tamaño fijo para almacenar toda la estructura. Sin embargo, los valores de los campos de tipostringse almacenan en el heap, ya que en Go los strings son cabeceras que apuntan a una secuencia de bytes en memoria dinámica, mientras queedad, que es del tipouintse almacena también en el Segmento de Datos.
p21p2 := Persona{nombre: "Pepe", edad: 23}Almacenamiento: Stack (estructura) y Heap (cadenas)
Justificación:
p2es una variable local definida dentro demain, por lo que se crea en el marco de pila (stack frame) de dicha función. Al igual que conp1, el contenido de los campos de tipostring(“Pepe”) se almacena en el heap.
p31p3 := &p2Almacenamiento: Stack
Justificación:
p3es un puntero ap2, definido como variable local enmain. Se almacena en el stack y apunta a la misma estructuraPersonaquep2. A través dep3se modificanp2.apellidoa"Martinez"yp2.direccionaDireccion{"Av. Corrientes", "CABA", "Buenos Aires", 1050}.
p41 2p4 := Persona{"Juan", "Gonzalez", 34, Direccion{"Valentín Gómez", "Caseros", "Buenos Aires", 742}}Almacenamiento: Stack (estructura) y Heap (cadenas)
Justificación:
p4es una variable local de tipoPersonaque se almacena en el stack. Los valores de tipostring("Juan","Gonzalez", etc.) se almacenan en el heap. Posteriormente, se reasigna su campodireccionconDireccion{"Mariano Acosta", "Gonzalez Catán", "Buenos Aires", 6420}.
Resumen general:
| Variable | Ubicación | Justificación |
|---|---|---|
num | Segmento de Datos | Variable global simple. |
p1 | Segmento de Datos (header) / Heap (str) | Estructura global, contenido de cadenas en heap. |
p2 | Stack (estructura) / Heap (cadenas) | Variable local, contenido de cadenas en heap. |
p3 | Stack | Puntero local a p2. |
p4 | Stack (estructura) / Heap (cadenas) | Variable local, contenido de cadenas en heap. |
En Go, el compilador y el recolector de basura (GC) optimizan el uso del stack y el heap. Las estructuras simples y de corta duración suelen estar en el stack, mientras que los datos más complejos o de mayor duración (como cadenas) se almacenan en el heap.
Go utiliza un recolector de basura concurrente para liberar memoria automáticamente. Es concurrente porque la mayor parte del trabajo de limpieza ocurre en paralelo con la ejecución del programa. Esto permite que el programador no tenga que liberar la memoria manualmente, evitando errores comunes como el acceso a memoria inválida.
Cuando existen datos en la memoria dinámica que ya no son accesibles (no están referenciados desde la pila ni el segmento de datos), el GC los marca para su eliminación y eventual liberación.
Para coordinar ciertos pasos del proceso de limpieza, el GC realiza pausas extremadamente breves denominadas stop-the-world (generalmente menores a un milisegundo). Go se destaca por optimizar estas pausas para que no interfieran con el rendimiento percibido del sistema:
Minimiza las pausas para optimizar la latencia.
Usa múltiples núcleos de CPU para ejecutar la recolección en paralelo.
La mayor parte del marcado de objetos ocurre mientras el programa sigue corriendo.
Detecta y elimina referencias a objetos no utilizados de forma eficiente.
4Ejercicios¶
Mapa de memoria. Dado el siguiente código, dibujá un esquema de la memoria indicando en qué segmento (datos, stack o heap) se almacena cada variable y su contenido:
1 2 3 4 5 6 7 8 9 10 11 12var global int = 42 type Punto struct { X, Y int } func main() { a := 10 b := &a c := Punto{X: 3, Y: 7} d := "hola" }Escape analysis. Para cada una de las siguientes funciones, indicá si la variable creada escapa al heap o se queda en el stack. Justificá:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15func f1() int { x := 5 return x } func f2() *int { x := 5 return &x } func f3() { s := make([]int, 10) s[0] = 1 fmt.Println(s[0]) }Strings en heap. Explicá por qué el contenido de un
stringen Go siempre se almacena en el heap, incluso cuando la variable que lo contiene es local. ¿Qué parte de la variable queda en el stack?Stack frames. Dada la secuencia de llamadas
main()→calcular()→sumar(a, b int), describí el contenido del stack en cada paso. Indicá qué datos contiene cada stack frame (parámetros, variables locales, dirección de retorno).Garbage collector. Explicá brevemente:
Qué problema resuelve el garbage collector concurrente de Go.
Qué significa stop-the-world y por qué las pausas son del orden de microsegundos en Go.
Qué ventaja tiene un GC concurrente frente a uno no concurrente.