Gestión de Memoria y Garbage Collector en Python

Python implementa un sistema de tres capas para la gestión automática de memoria. Esta implementación libera al programador de la complejidad de asignar y liberar memoria manualmente, permitiéndole enfocarse en la lógica de negocio. A diferencia de lenguajes de bajo nivel como C o C++, que requieren gestión manual de memoria ofreciendo control directo sobre el hardware (necesario para drivers y sistemas embebidos), Python prioriza la prevención de errores comunes de gestión manual y la productividad del desarrollador mediante automatización.

Este artículo describe CPython (la implementación estándar de Python). Otras implementaciones como PyPy, Jython e IronPython tienen sistemas diferentes de gestión de memoria.

Arquitectura de Gestión de Memoria

Python utiliza una estrategia de gestión de memoria en múltiples capas:

Capa 1: Sistema Operativo -> Objetos grandes (>512 bytes) malloc() / free()
Capa 2: PyMalloc (Allocator de Python) -> Objetos pequeños (≤512 bytes)
Capa 3: Object Allocators -> Código Python (listas, dicts, objetos, etc.)

¿Qué es un Allocator?

Un allocator es el sistema que asigna y libera bloques de memoria. Actúa como “administrador de memoria” decidiendo:

  • Dónde ubicar nuevos datos en memoria
  • Cómo organizar el espacio disponible
  • Cuándo liberar memoria que ya no se usa

Capa 1: Allocator del Sistema Operativo

  • En el nivel más bajo, Python usa malloc() y free() del sistema operativo.
    • malloc(tamaño): “Memory Allocation” – Pide un bloque de memoria al sistema operativo.
    • free(puntero): Libera un bloque de memoria previamente asignado.
  • Se usa para objetos muy grandes (> 512 bytes).
// Ejemplo en C (lo que Python hace internamente)
int* numeros = malloc(1024);  // Pedir 1024 bytes
// ... usar la memoria ...
free(numeros);  // Devolver memoria al SO

Nota: Cuando se descarga Python (https://www.python.org) se obtiene CPython el cual está escrito en C, la implementación estándar y más común de Python.

Capa 2: Python Memory Manager (pymalloc)

  • Es un allocator optimizado específico de Python que recicla bloques de memoria para evitar solicitudes repetidas al sistema operativo.
  • Destinado a objetos pequeños (≤ 512 bytes) como por ejemplo ints, strings cortos y tuplas pequeñas.
  • Trabaja organizado en pools (memoria pre-dividida en bloques iguales) y arenas (contenedores grandes de 1 MB en 64-bit).
    • Por ejemplo:
      • Python pide arenas al SO con malloc(1 MB).
      • Divide cada arena en pools de 4 KB (hasta aproximadamente 256 pools por arena, menos algunos bytes de overhead).
      • Una vez pedido el espacio, lo reutiliza indefinidamente.
  • Reduce fragmentación al usar bloques de tamaño fijo para que no queden partes de memoria inutilizables.

¿Por qué se fragmenta la memoria con malloc normal?

Cuando asignas y liberas objetos de diferentes tamaños, quedan “espacios” que pueden ser muy pequeños para reutilizar:

Malloc normal (fragmentado):

[10][LIBRE-5][20][LIBRE-3][15][LIBRE-8][25]
     ^            ^             ^
     Espacios inútiles: ninguno sirve para un objeto de 20 bytes

Pymalloc (sin fragmentación):

Pool de 32 bytes:
[32][LIBRE-32][32][LIBRE-32][32]
     ^              ^
     Ambos espacios sirven para cualquier objeto ≤ 32 bytes
  • Mejora el rendimiento siendo más rápido que malloc ya que realiza menos syscalls o llamadas al sistema operativo. Tiene un mejor uso de caché o menos acceso a la RAM.

Capa 3: Object Allocator

  • Capa de alto nivel para tipos específicos de tamaño pequeño.
  • Cachés especializados para objetos frecuentes o comunes en lugar de crearlos y destruirlos constantemente.

¿Por qué Python usa malloc/free directamente para objetos grandes?

1. El overhead de pymalloc genera costos adicionales

Para objetos grandes, las optimizaciones de pymalloc no aportan beneficios y solo añaden complejidad innecesaria.

import sys

# Objeto pequeño (< 512 bytes)
pequeño = [1, 2, 3]  # ~100 bytes
print(f"Tamaño: {sys.getsizeof(pequeño)} bytes")
# Python usa: pymalloc (Capa 2)

# Objeto grande (> 512 bytes)
grande = [i for i in range(1000)]  # ~8000 bytes
print(f"Tamaño: {sys.getsizeof(grande)} bytes")
# Python usa: malloc del SO (Capa 1)

sys.getsizeof(): Esta función retorna el tamaño del contenedor, NO incluye el tamaño de los objetos contenidos ya que lo que se almacena es la referencia o el puntero (8 bytes) al objeto.
Los objetos reales viven independientemente en el heap de memoria (una gran región de memoria dinámica)

Pymalloc tiene un costo de gestión:

Headers de pools

Cada pool de 4 KB necesita guardar información administrativa:

  • ¿Cuántos bloques están libres?
  • ¿De qué tamaño son los bloques? (16, 32, 48…)
  • Punteros al siguiente pool.

Costo: Aproximadamente 32-48 bytes por pool según la versión de Python (overhead fijo independiente del contenido)

Metadata de arenas

Cada arena de 1 MB necesita rastrear:

  • ¿Qué pools contiene?
  • ¿Cuáles están llenos, parcialmente usados o vacíos?
  • Dirección de memoria base.

Costo: ~100-200 bytes por arena

Lógica de búsqueda de bloques

Cuando pides memoria, Python debe:

  1. Calcular el tamaño de la clase (¿es un objeto de 88 bytes? → usar clase 5 de 96 bytes)
  2. Buscar un pool con bloques libres de ese tamaño.
  3. Si no existe, crear nuevo pool.
  4. Actualizar contadores.

Costo: Ciclos de CPU extra (tiempo de procesamiento)

Cuantización (redondeo)

Si pides 88 bytes, pymalloc te da 96 bytes (el tamaño de clase más cercano).

Por ejemplo:
Pides 88 bytes → redondea a 96 bytes
Pides 1 byte → redondea a 16 bytes
Pides 50 bytes → redondea a 64 bytes

Costo: 8 bytes desperdiciados (9% del objeto)

Clases de tamaño

Las clases de tamaño varían según la versión de Python y arquitectura:

  • Python < 3.8 o 32-bit: 8, 16, 24, 32, 40, 48, …, 512 bytes (64 clases)
  • Python 3.8+ en 64-bit: 16, 32, 48, 64, 80, 96, …, 512 bytes (32 clases)
  • Cada pool contiene bloques de un solo tamaño.

Para validar las clases existentes en tu sistema, ejecuta el siguiente código:

import sys
sys._debugmallocstats()  

2. Los objetos grandes son menos frecuentes

La mayoría de objetos en Python son pequeños (enteros, referencias, strings cortos)

import sys

# La mayoría de objetos en Python son pequeños
objetos_pequeños = [
    42,                    # int: ~28 bytes
    "hola",               # str: ~54 bytes
    (1, 2),               # tuple: ~56 bytes
    {"a": 1},             # dict pequeño: ~232 bytes
]

for obj in objetos_pequeños:
    print(f"{type(obj).__name__:10} → {sys.getsizeof(obj):4} bytes")

print("\nObjetos grandes:")
# Estos van directo al SO
grandes = [
    "x" * 1000,           # str grande
    list(range(1000)),    # lista grande
    {i: i for i in range(100)}  # dict grande
]

for obj in grandes:
    print(f"{type(obj).__name__:10} → {sys.getsizeof(obj):6} bytes")

Conteo de Referencias (Reference Counting)

El mecanismo principal de gestión de memoria en Python es el conteo de referencias. Cada objeto en Python tiene un campo ob_refcount que lleva cuenta de cuántas referencias apuntan a ese objeto.

Cómo funciona

import sys

# Crear un objeto
a = [1, 2, 3]
print(sys.getrefcount(a))  # Mínimo 2: variable 'a' + argumento temporal
# Puede ser mayor debido a referencias internas del intérprete

# Crear otra referencia
b = a
print(sys.getrefcount(a))  # Se incrementa (ahora tenemos 'a', 'b' + argumento)

# Eliminar una referencia
del b
print(sys.getrefcount(a))  # Decrementa de vuelta

# Cuando el conteo llega a 0, el objeto se libera inmediatamente
del a  # Ahora el objeto es liberado

Ventajas del conteo de referencias

  • Liberación inmediata de memoria cuando refcount = 0
  • Determinístico: sabes exactamente cuándo se libera la memoria
  • Permite implementar __del__() de forma predecible

Desventajas

  • Overhead de memoria: cada objeto necesita espacio para el contador
  • Overhead de procesamiento: incrementar/decrementar en cada asignación
  • No detecta ciclos de referencias (problema fundamental)

El Problema de los Ciclos de Referencias

El conteo de referencias tiene una debilidad fundamental: no puede detectar ciclos.

# Crear un ciclo
class Nodo:
    def __init__(self, valor):
        self.valor = valor
        self.siguiente = None

# Crear un ciclo
nodo1 = Nodo(1)
nodo2 = Nodo(2)
nodo1.siguiente = nodo2
nodo2.siguiente = nodo1  # Ciclo

# Incluso si eliminamos las referencias externas:
del nodo1, nodo2
# Los objetos siguen teniendo refcount > 0 porque se referencian mutuamente
# Sin recolector de basura, esto sería un memory leak o memoria que nunca se libera

Recolector de Basura (Garbage Collector)

Para solucionar el problema de los ciclos, Python implementa un recolector de basura generacional que complementa el conteo de referencias.

Algoritmo Generacional

Python divide los objetos en tres generaciones basándose en la hipótesis de que:

  • La mayoría de objetos mueren jóvenes.
  • Objetos que sobreviven múltiples colecciones probablemente vivirán mucho tiempo
import gc

# Ver información sobre generaciones
print(gc.get_count())  # (contador_gen0, contador_gen1, contador_gen2)
print(gc.get_threshold())  # Umbrales para activar colección (700, 10, 10)

# Generación 0: objetos nuevos
# Generación 1: objetos que sobrevivieron 1+ colecciones
# Generación 2: objetos que sobrevivieron múltiples colecciones

Funcionamiento del GC

  1. Generación 0 (más joven):
  • Se revisa frecuentemente.
  • Umbral típico: ~700 asignaciones netas.
  • Colecciones muy rápidas.
  1. Generación 1 (media):
  • Se revisa menos frecuentemente.
  • Se ejecuta después de 10 colecciones de generación 0.
  1. Generación 2 (más vieja):
  • Se revisa raramente.
  • Se ejecuta después de 10 colecciones de generación 1.
  • Contiene objetos de larga vida (por ejemplo, módulos u objetos globales).

Cuándo se ejecuta el GC

import gc

# El GC se ejecuta automáticamente cuando:
# 1. Se excede el umbral de objetos en gen0 (~700 objetos nuevos)
# 2. Se llama explícitamente gc.collect()
# 3. El programa termina

# Ver umbrales actuales
print(gc.get_threshold())  # (700, 10, 10) -> en mi caso da (2000, 10, 10) 
# gen0: 700 asignaciones
# gen1: después de 10 colecciones de gen0
# gen2: después de 10 colecciones de gen1

# Modificar umbrales (no recomendado sin profiling previo)
gc.set_threshold(800, 15, 15)

Detección de Ciclos

El GC usa un algoritmo de marcado y barrido (mark and sweep) que, en esencia, consiste en marcar lo que sigue siendo accesible y eliminar lo que no fue marcado:

# El GC puede detectar este ciclo:
class ContenedorCircular:
    def __init__(self):
        self.referencia = self  # Referencia circular

obj = ContenedorCircular()
del obj  # GC lo limpiará en la próxima colección

Advertencia sobre __del__(): Si una clase en un ciclo de referencias tiene un método __del__() personalizado, el GC no puede recolectar el ciclo de forma segura. Esto puede causar memory leaks.

class ProblematicaGC:
    def __init__(self):
        self.referencia = self

    def __del__(self):
        print("Siendo destruido")
        # Si está en un ciclo de referencias,
        # el GC no puede recolectar el ciclo si tiene __del__()

Control Manual del GC

import gc

# Deshabilitar GC (útil para debugging o evitar pausas)
gc.disable()

# Ejecutar colección manualmente
n_collected = gc.collect()  # Fuerza colección completa
print(f"Objetos liberados: {n_collected}")

# Habilitar de nuevo
gc.enable()

# Ver objetos que el GC está rastreando
print(f"Objetos rastreados: {len(gc.get_objects())}")  # Miles de objetos

# Encontrar referencias a un objeto
obj = [1, 2, 3]
refs = gc.get_referrers(obj)
print(f"Referencias a obj: {refs}")

Object Interning y Caching

Python optimiza el uso de memoria reutilizando instancias de objetos inmutables comunes mediante una técnica llamada interning.
En CPython, los enteros en el rango de −5 a 256 se crean una sola vez y se reutilizan durante toda la ejecución del programa:

# Pequeños enteros están pre-creados (singleton)
a = 256
b = 256
print(a is b)  # True - mismo objeto en memoria

a = 257
b = 257
print(a is b)  # False en CPython (pero puede variar según contexto)

# String interning para identificadores
s1 = "hola"
s2 = "hola"
print(s1 is s2)  # True - strings simples se internalizan

Nota sobre string interning: Esto es una optimización de CPython, no garantizada por el lenguaje. Funciona para:

  • Literales de string en el código.
  • Strings que parecen identificadores (solo letras, números, underscore)
  • No funciona siempre para strings creados en runtime.
# Interning explícito
import sys
s1 = sys.intern("cadena larga que queremos reutilizar")
s2 = sys.intern("cadena larga que queremos reutilizar")
print(s1 is s2)  # True - ahora comparten memoria

Objetos pre-creados en CPython

  • Enteros de -5 a 256 (singleton)
  • Empty tuple ()
  • Strings de un solo carácter
  • True, False, None

Weak References

En Python, para evitar que ciertos objetos se mantengan vivos innecesariamente, se pueden usar referencias débiles (weak references).
Estas referencias permiten acceder a un objeto sin aumentar su contador de referencias, de modo que el recolector de basura puede eliminarlo cuando ya no haya referencias fuertes:

import weakref

class Pesado:
    def __init__(self, datos):
        self.datos = datos

# Referencia normal (fuerte)
obj = Pesado([1] * 1000000)

# Referencia débil (no incrementa refcount)
ref_debil = weakref.ref(obj)

print(ref_debil())  # Acceder al objeto: <__main__.Pesado object>

# El objeto sigue vivo
del obj
print(ref_debil())  # None - objeto fue liberado

# Diccionarios débiles
cache = weakref.WeakValueDictionary()
obj = Pesado([1, 2, 3])
cache['key'] = obj

print('key' in cache)  # True
del obj
print('key' in cache)  # False - se eliminó automáticamente

Casos de uso para weak references

  • Cachés que no deben mantener objetos vivos artificialmente; los objetos se eliminan automáticamente cuando ya no existen referencias fuertes.
  • Listeners u observers que no deben impedir la recolección de basura del objeto observado.
  • Almacenamiento de metadatos asociados a objetos sin extender su tiempo de vida (lifetime).

Context Managers y Gestión de Recursos

Para recursos externos (archivos, conexiones), Python usa context managers:

# Sin context manager (propenso a leaks)
archivo = open('datos.txt')
datos = archivo.read()
archivo.close()  # ¿Qué pasa si hay excepción antes?

# Con context manager (seguro)
with open('datos.txt') as archivo:
    datos = archivo.read()
# archivo.close() se llama automáticamente, incluso con excepciones

# Context manager personalizado
class GestorRecurso:
    def __enter__(self):
        print("Adquiriendo recurso")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Liberando recurso")
        return False  # Re-raise excepciones

with GestorRecurso() as recurso:
    print("Usando recurso")

Memory Profiling y Debugging

En Python, el manejo de memoria puede volverse complejo en aplicaciones grandes o de alto rendimiento. Para identificar consumo excesivo, fugas de memoria o objetos que permanecen vivos inesperadamente, existen varias herramientas y técnicas de memory profiling y depuración de referencias.

1. tracemalloc (stdlib)

Permite medir el uso actual y pico de memoria, y rastrear qué líneas

import tracemalloc

tracemalloc.start()

# Código que queremos analizar
lista = [i for i in range(1000000)]

current, peak = tracemalloc.get_traced_memory()
print(f"Uso actual: {current / 1024 / 1024:.2f} MB")
print(f"Peak: {peak / 1024 / 1024:.2f} MB")

# Ver top allocations
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:3]:
    print(stat)

tracemalloc.stop()

2. Memory_profiler (librería externa)

Proporciona reportes línea por línea del uso de memoria de funciones específicas, ideal para analizar el impacto de bloques de código concretos.

from memory_profiler import profile

@profile
def funcion_pesada():
    lista = [i**2 for i in range(100000)]
    return sum(lista)

funcion_pesada()
# Genera reporte línea por línea de uso de memoria

3. objgraph (visualizar referencias – librería externa)

Visualiza referencias y dependencias entre objetos, ayudando a detectar fugas de memoria y entender por qué ciertos objetos siguen vivos.

import objgraph

class MiClase:
    pass

x = MiClase()
y = [x]

# Ver referencias a un objeto
objgraph.show_refs([x], filename='refs.png')

# Ver backreferences (qué mantiene vivo al objeto)
objgraph.show_backrefs([x], filename='backrefs.png')

# Encontrar memory leaks
objgraph.show_most_common_types(limit=10)

Mejores Prácticas y Optimizaciones

1. Liberar referencias grandes explícitamente

import numpy as np

# Array grande
datos = np.zeros((10000, 10000))

# Procesar...
resultado = procesar(datos)

# Liberar explícitamente
del datos  # Recomendado para objetos muy grandes

2. Usar generadores para secuencias grandes

# Malo: crea lista completa en memoria
suma = sum([i**2 for i in range(10000000)])
# [i**2 for i in range(10000000)] -> esto es una lista por comprensión.    
# Python crea toda la lista en memoria de una vez, con 10 millones de elementos.    
# Cada elemento ocupa espacio en memoria (y la lista tiene overhead adicional).    
# Esto puede consumir gigabytes de RAM si la secuencia es muy grande.

# Bueno: generator no almacena todo en memoria
suma = sum(i**2 for i in range(10000000))
# i**2 for i in range(10000000)-> esto es un generator expression. 
# Python no crea una lista completa, sino que genera cada elemento sobre la marcha.  
# sum() toma un elemento a la vez, lo suma, y luego descarta ese elemento.   
# Esto mantiene el uso de memoria constante, sin importar cuán grande sea la secuencia.

3. Reutilizar objetos en lugar de crear nuevos

# Malo: crea muchos strings temporales
resultado = ""
for i in range(1000):
    resultado += str(i)  # Crea nuevo string cada vez

# Bueno: usa lista y join
partes = []
for i in range(1000):
    partes.append(str(i))
resultado = "".join(partes)

4. Usar __slots__ para clases con muchas instancias

# Sin slots: cada instancia tiene un __dict__
class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Con slots: menor uso de memoria (40-50% menos), elimina el __dict__ por instancia
class PuntoOptimizado:
    __slots__ = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

# Para millones de instancias, esto ahorra gigabytes
puntos = [PuntoOptimizado(i, i) for i in range(1000000)]

5. Considerar numpy para datos numéricos

import numpy as np

# Lista Python: 8 bytes por entero + overhead
lista = [i for i in range(1000000)]

# Array numpy: 4 bytes por entero, contiguos
array = np.arange(1000000, dtype=np.int32)
# Uso de memoria: ~1/4 de la lista Python

Limitaciones y Consideraciones

Global Interpreter Lock (GIL)

El Global Interpreter Lock (GIL) es un mecanismo que garantiza que solo un thread de Python ejecute bytecode a la vez, incluso en sistemas con múltiples núcleos.
Esto se hace para proteger estructuras internas críticas, principalmente el conteo de referencias, evitando errores de memoria y condiciones de carrera.

  • Limita el rendimiento de programas CPU-bound (procesamiento intensivo en CPU) que usan múltiples hilos, ya que no pueden ejecutar bytecode (es lo que ejecuta la máquina virtual de Python) de manera concurrente.
  • No afecta tanto programas I/O-bound (espera de recursos), donde los hilos pueden alternar mientras esperan operaciones de red, disco u otros recursos externos.
  • A partir de Python 3.13, existen builds con “free-threading” (PEP 703) que permiten ejecutar bytecode en múltiples threads simultáneamente, eliminando esta limitación y permitiendo true multithreading en Python.

Memory Leaks posibles

  1. Ciclos con __del__() personalizado.
  2. Referencias en excepciones no manejadas.
  3. Closures que capturan contexto grande.
  4. Variables globales que crecen indefinidamente.
# Memory leak por closure
def crear_closure():
    datos_grandes = [0] * 1000000

    def funcion():
        # Captura datos_grandes porque lo referencia
        print(len(datos_grandes))
        return 42

    return funcion

# datos_grandes permanece en memoria mientras exista funcion
f = crear_closure()

Nota: Python solo captura variables en closures que realmente se referencian. Si una función anidada no usa una variable del scope externo, no la captura.


Comparación con Otros Lenguajes

El manejo de memoria en Python se diferencia significativamente de otros lenguajes:

Python vs C/C++

  • C/C++: Memoria gestionada manualmente con malloc/free o new/delete. Esto permite alta velocidad, pero es propenso a errores como fugas de memoria o punteros colgantes (dirección de memoria que ya no es válida).
  • Python: Memoria gestionada automáticamente mediante reference counting más un GC generacional para ciclos. Esto reduce errores y simplifica el desarrollo, aunque introduce overhead y cierta pérdida de velocidad.

Python vs Java

  • Java: Solo GC generacional puro. Menor overhead por objeto y sin necesidad de contar referencias, pero puede generar pausas más largas durante la recolección de basura (stop-the-world).
  • Python: Refcounting + GC generacional. Generalmente produce pausas más cortas y predecibles, pero con más overhead de memoria por objeto.

Python vs Go

  • Go: GC concurrente que permite ejecutar múltiples goroutines mientras la recolección ocurre, ideal para programas altamente concurrentes.
  • Python: GC stop-the-world combinado con refcounting. Más simple y predecible, pero las pausas pueden ser visibles en programas CPU-bound con varios threads.

Cuando se habla de pausas cortas o largas en el contexto de recolección de basura (GC), se refiere al tiempo durante el cual la ejecución normal del programa se detiene para que el GC pueda limpiar la memoria. Esto se conoce como “stop-the-world”.


Conclusión

La gestión automática de memoria en Python ofrece un equilibrio entre rendimiento y facilidad de uso, permitiendo a los desarrolladores concentrarse en la lógica del negocio sin preocuparse por la liberación manual de memoria.

El sistema híbrido de reference counting combinado con un garbage collector generacional proporciona varias ventajas clave:

  • Liberación inmediata para la mayoría de los objetos sin ciclos, garantizando eficiencia en el uso de memoria.
  • Detección y recolección de ciclos para casos más complejos, evitando fugas de memoria.
  • Optimizaciones específicas, como pymalloc y object interning, que mejoran el rendimiento y reducen el overhead en los escenarios más comunes.

Contras / Limitaciones

  • Overhead de memoria: Cada objeto mantiene un contador de referencias, lo que aumenta ligeramente el consumo de memoria.
  • Pausas visibles en GC: Aunque cortas, la recolección de ciclos puede generar pausas perceptibles en programas CPU-bound grandes.
  • Limitaciones en multithreading: El GIL impide el “true parallelism” de hilos que ejecutan bytecode, limitando la eficiencia en programas altamente concurrentes.

Referencias

Documentación Oficial de Python

  1. PEP 445 – Memory Allocation API – Arquitectura de gestión de memoria, Capas 1 y 2.
  2. Python C API – Memory Management – PyMalloc, clases de tamaño, overhead.
  3. Python C API – Integer Objects – Object interning (enteros -5 a 256)
  4. PEP 703 – Making the GIL Optional – Global Interpreter Lock, Python 3.13+ free-threading.
  5. gc module – Garbage Collector – Recolector de basura, gc.collect(), gc.get_threshold().
  6. weakref module – Weak References – Referencias débiles.
  7. tracemalloc module – Memory Profiling – Memory profiling y debugging.
  8. Introduction Python– Introducción Python.

Código Fuente CPython

  1. Objects/obmalloc.c – Implementación de pymalloc – Clases de tamaño, implementación de pymalloc, ALIGNMENT.
  2. Issue #80799 – clang expects 16-byte alignment – Cambio a 16 bytes en Python 3.8+
  3. Commit f0be4bb – pymalloc alignment fix – Evidencia del cambio de alineación.

Artículos Técnicos

  1. Memory Management in Python – Artem Golubin – Explicación detallada de pools, arenas y bloques en pymalloc.
  2. https://cs-fundamentals.com/c-programming/memory-layout-of-c-program-code-data-segments – Segmentos de memoria por ejemplo Heap y Stack.

String Interning

  1. CPython InternalDocs – String Interning – Documentación de CPython sobre string interning.
  2. Guide to String Interning in Python – Stack Abuse – Explicación de cuándo ocurre interning automático.
  3. The Internals of Python String Interning – Análisis del código fuente de CPython.
  4. String Interning Discovery – Función all_name_chars() y reglas de interning.

Garbage Collector

  1. Java Garbage Collector – Java Garbage Collector.
  2. Go Memory Management – Go Memoria Management.