Articulo de referencia

Gestión manual de la memoria

En informática , la gestión manual de memoria se refiere al uso de instrucciones manuales por parte del programador para identificar y liberar objetos no utilizados , o basura ....

En informática , la gestión manual de memoria se refiere al uso de instrucciones manuales por parte del programador para identificar y liberar objetos no utilizados , o basura . Hasta mediados de la década de 1990, la mayoría de los lenguajes de programación utilizados en la industria admitían la gestión manual de memoria, aunque la recolección de basura existe desde 1959, cuando se introdujo con Lisp . [ 1 ] Hoy en día, sin embargo, los lenguajes con recolección de basura como Java son cada vez más populares y los lenguajes Objective-C y Swift proporcionan una funcionalidad similar a través del conteo automático de referencias . Los principales lenguajes de gestión manual que todavía se utilizan ampliamente hoy en día son C y C++ – véase asignación dinámica de memoria en C .

Descripción

Muchos lenguajes de programación utilizan técnicas manuales para determinar cuándo asignar un nuevo objeto desde el almacenamiento libre. C utiliza la mallocfunción; C++ y Java utilizan el newoperador; y muchos otros lenguajes (como Python) asignan todos los objetos desde el almacenamiento libre. Determinar cuándo se debe crear un objeto ( creación de objeto ) es generalmente trivial y sin problemas, aunque técnicas como los grupos de objetos implican que un objeto puede crearse antes de su uso inmediato. El verdadero desafío es la destrucción de objetos : determinar cuándo un objeto ya no es necesario (es decir, es basura) y organizar que su almacenamiento subyacente se devuelva al almacenamiento libre para su reutilización. En la asignación manual de memoria, esto también lo especifica manualmente el programador; mediante funciones como free()en C, o el deleteoperador en C++, lo que contrasta con la destrucción automática de objetos almacenados en variables automáticas , en particular las variables locales (no estáticas) de las funciones, que se destruyen al final de su ámbito en C y C++.

Técnicas de gestión de la memoria manual

Los programadores emplean diversas técnicas manuales para controlar el ciclo de vida de los objetos y reutilizar la memoria. Algunos patrones comunes son:

  • Asignación y desasignación explícitas : el programador llama a funciones como malloc()y free()en C, o a los operadores newy deleteen C++, para adquirir memoria del montón y devolverla al montón .
  • Áreas de memoria (memoria basada en regiones): muchos objetos se asignan desde un único bloque contiguo que se libera de una sola vez, lo que simplifica la gestión del ciclo de vida y mejora la localidad.
  • Búferes temporales : asignaciones temporales y de corta duración que se reciclan entre iteraciones o fotogramas, a menudo utilizadas en el desarrollo de videojuegos y sistemas embebidos para evitar la interacción frecuente con el montón.
  • Grupos de objetos : colecciones preasignadas de objetos reutilizables que se distribuyen y se devuelven, eliminando la sobrecarga de asignación/desasignación por objeto.
  • Asignadores personalizados : estrategias de asignación especializadas (por ejemplo, asignadores de pila, asignadores de lista libre, asignadores de bloques) adaptadas a patrones de uso particulares para mejorar el rendimiento o reducir la fragmentación.
  • La adquisición de recursos es la inicialización (RAII) : vincular la vida útil de un recurso (incluida la memoria) al ámbito de un objeto adquiriéndolo en el constructor y liberándolo en el destructor; ampliamente utilizado en C++ y Rust (ver §  La adquisición de recursos es la inicialización ).
  • Placement new (C++) – Construir un objeto en memoria ya asignada, separando la asignación de memoria de la construcción del objeto.
  • Defer – Una construcción de lenguaje que programa la ejecución de código de limpieza cuando finaliza un ámbito o una función; se encuentra en Go, Zig y C29 (ver §  Defer ).

Las técnicas manuales suelen proporcionar al programador un control preciso sobre el rendimiento y el uso de los recursos, pero también introducen el riesgo de errores como punteros colgantes , fugas de memoria y comportamiento indefinido si se utilizan incorrectamente.

Gestión manual y corrección

Se sabe que la gestión manual de la memoria, cuando se utiliza incorrectamente, puede introducir varios tipos importantes de errores en un programa, en particular violaciones de la seguridad de la memoria o fugas de memoria . Estos son una fuente importante de vulnerabilidades de seguridad . [ 2 ]

  • Cuando un objeto no utilizado nunca se libera, se habla de una fuga de memoria . En algunos casos, las fugas de memoria pueden ser tolerables, como en un programa que consume una cantidad limitada de memoria durante su ejecución, o en un programa de corta duración que depende del sistema operativo para liberar sus recursos al finalizar. Sin embargo, en muchos casos, las fugas de memoria ocurren en programas de larga duración, y en tales casos se pierde una cantidad ilimitada de memoria. Cuando esto sucede, el tamaño de la memoria libre disponible disminuye progresivamente; cuando finalmente se agota, el programa falla.
  • Un fallo catastrófico del sistema de gestión de memoria dinámica puede producirse cuando la memoria subyacente de un objeto se borra más de una vez; un objeto se destruye explícitamente más de una vez; cuando, al usar un puntero para manipular un objeto no asignado en la memoria libre, un programador intenta liberar la memoria subyacente del objeto de destino de dicho puntero; o cuando, al manipular un objeto mediante un puntero a otra área de memoria arbitraria gestionada por una tarea, hilo o proceso externo desconocido, un programador corrompe el estado de ese objeto, posiblemente de tal manera que escriba fuera de sus límites y corrompa sus datos de gestión de memoria. El resultado de tales acciones puede incluir corrupción del montón , destrucción prematura de un objeto diferente (y recién creado) que casualmente ocupa la misma ubicación en la memoria que el objeto borrado varias veces, fallos del programa debido a un fallo de segmentación (violación de la protección de memoria ) y otras formas de comportamiento indefinido .
  • Los punteros a objetos eliminados se convierten en punteros no válidos si se utilizan después de su eliminación; intentar utilizar dichos punteros puede provocar errores difíciles de diagnosticar.

Se sabe que los lenguajes que utilizan exclusivamente la recolección de basura evitan los dos últimos tipos de defectos. Aún pueden producirse fugas de memoria (y las fugas limitadas ocurren con frecuencia en la recolección de basura generacional o conservadora), pero generalmente son menos graves que las fugas de memoria en los sistemas manuales.

La adquisición de recursos es la inicialización

La gestión manual de la memoria tiene una ventaja en cuanto a la corrección, que es que permite la gestión automática de recursos a través del paradigma de adquisición e inicialización de recursos (RAII, por sus siglas en inglés).

Esto ocurre cuando los objetos poseen recursos del sistema escasos (como recursos gráficos, descriptores de archivo o conexiones a bases de datos) que deben liberarse al destruirse el objeto; es decir, cuando la duración de la propiedad del recurso debe estar ligada a la del objeto. Los lenguajes con gestión manual pueden organizar esto adquiriendo el recurso durante la inicialización del objeto (en el constructor) y liberándolo durante su destrucción (en el destructor ), lo cual sucede en un momento preciso. Esto se conoce como Adquisición de Recursos durante la Inicialización (RAII, por sus siglas en inglés).

Esto también se puede usar con el conteo de referencias determinista . En C++, esta capacidad se aprovecha aún más para automatizar la desasignación de memoria dentro de un marco que de otro modo sería manual; el uso de punteros inteligentes en la biblioteca estándar del lenguaje para realizar la gestión de memoria es un paradigma común. En lenguajes como Rust , que imponen la propiedad estricta, RAII se convierte en el comportamiento predeterminado para la gestión de recursos. [ 3 ]

Este enfoque no es utilizable en la mayoría de los lenguajes con recolección de basura, especialmente en aquellos con recolección de basura de rastreo o conteo de referencias más avanzado, debido a que la finalización no es determinista y, a veces, ni siquiera ocurre. Es decir, es difícil definir (o determinar) cuándo o si se llamará a un método finalizador ; esto se conoce comúnmente como el problema del finalizador . Java y otros lenguajes que implementan un recolector de basura suelen usar la administración manual de recursos del sistema escasos, además de la memoria, a través del patrón `dispose` : se espera que cualquier objeto que administre recursos implemente el dispose()método `dispose`, que libera dichos recursos y marca el objeto como inactivo. Se espera que los programadores lo invoquen dispose()manualmente según corresponda para evitar la "fuga" de recursos gráficos escasos. Para los recursos de pila (recursos adquiridos y liberados dentro de un solo bloque de código), esto se puede automatizar mediante varias construcciones del lenguaje, como `dispose` de Python with, `dispose` de C# using(que llama Dispose()al método `dispose` de un objeto) o ` trydispose` de Java (utilizable en cualquier objeto que implemente java.lang.AutoCloseabley llame a su close()método `dispose`).

Aplazar

Un mecanismo de aplazamiento permite posponer la ejecución de un fragmento de código hasta más tarde, generalmente hasta el final del ámbito o de una función. Esto puede considerarse similar a un bloque `finally` en otros lenguajes de programación.

do

C (a partir de C29 ) introduce un defermecanismo. Cada deferbloque que se encuentra durante la ejecución se ejecuta al salir del ámbito (no cuando se encuentra), en el orden inverso al que se encontraron. [ 4 ]

#include <stddefer.h> #include <stdio.h> #include <stdlib.h>int processFile ( const char path []) { FILE * f = fopen ( path , "r" ); if ( ! f ) { return -1 ; }defer { printf ( "Cerrando archivo. \n " ); fclose ( f ); };char * buffer = malloc ( 1024 ); if ( ! buffer ) { return -2 ; }defer { printf ( "Liberando búfer. \n " ); free ( buffer ); };// Simular múltiples salidas anticipadas if ( fgets ( buffer , 1024 , f )) { return -3 ; // ambos diferimientos aún se ejecutan }if ( buffer [ 0 ] == '#' ) { return -4 ; // ambos diferimientos siguen ejecutándose }printf ( "Procesando: %s \n " , buffer ); return 0 ; }

C++ aún no se ha pronunciado sobre si integrará o no C defer, ya que RAII cubre este caso de uso.

Ir

En Go , deferse utiliza para programar una llamada a una función para que se ejecute cuando la función que la contiene finalice. Varias deferllamadas se ejecutan en orden último en entrar, primero en salir (LIFO).

paquete principalimportar "fmt"func main () { fmt . Println ( "start" )defer fmt . Println ( "primer aplazamiento" ) defer fmt . Println ( "segundo aplazamiento" )fmt.Println ( " fin " ) }

Zig

En Zig , deferes de nivel de bloque y se ejecuta cuando finaliza el ámbito actual, no solo la función. A diferencia de Go, Zig deferno genera sobrecarga.

const std = @import ( "std" );pub fn main () void { std . debug . print ( "start \n " , .{});{ defer std . debug . print ( "inner defer \n " , .{}); std . debug . print ( "inside block \n " , .{}); }std.debug.print ( "fin \n " , . { } ) ; }

Zig también incluye errdefer, que se ejecuta solo si la función finaliza con un error.

Actuación

Muchos defensores de la gestión manual de memoria argumentan que ofrece un rendimiento superior en comparación con técnicas automáticas como la recolección de basura . Tradicionalmente, la latencia era la mayor ventaja, pero esto ya no es así. La asignación manual suele tener una localidad de referencia superior .

También se sabe que la asignación manual es más apropiada para sistemas donde la memoria es un recurso escaso, debido a una recuperación más rápida. Los sistemas de memoria pueden saturarse con frecuencia cuando el tamaño del conjunto de trabajo de un programa se acerca al tamaño de la memoria disponible; los objetos no utilizados en un sistema con recolección de basura permanecen en un estado no recuperado durante más tiempo que en los sistemas gestionados manualmente, porque no se recuperan inmediatamente, lo que aumenta el tamaño efectivo del conjunto de trabajo.

La gestión manual presenta una serie de desventajas de rendimiento documentadas :

  • Las llamadas a deletefunciones como `delete` generan una sobrecarga cada vez que se realizan; esta sobrecarga puede amortizarse en los ciclos de recolección de basura. Esto es especialmente cierto en aplicaciones multihilo, donde las llamadas a funciones de eliminación deben estar sincronizadas.
  • La rutina de asignación puede ser más compleja y lenta. Algunos esquemas de recolección de basura, como los que utilizan compactación de montón , pueden mantener el espacio libre almacenado como una simple matriz de memoria (a diferencia de las implementaciones complejas que requieren los esquemas de administración manual).

La latencia es un tema controvertido que ha evolucionado con el tiempo. Los primeros recolectores de basura y las implementaciones sencillas tenían un rendimiento muy deficiente en comparación con la gestión manual de la memoria, pero los sofisticados recolectores de basura modernos suelen tener un rendimiento igual o superior al de la gestión manual de la memoria.

La asignación manual no sufre los largos tiempos de "pausa" que se producen en la recolección de basura simple que detiene el mundo, aunque los recolectores de basura modernos tienen ciclos de recolección que a menudo no se notan.

Tanto la gestión manual de memoria como la recolección de basura presentan tiempos de desasignación potencialmente ilimitados: la gestión manual, porque la desasignación de un solo objeto puede requerir la desasignación de sus miembros, y recursivamente de los miembros de sus miembros, etc.; mientras que la recolección de basura puede tener ciclos de recolección prolongados. Esto es especialmente problemático en sistemas en tiempo real , donde los ciclos de recolección ilimitados son generalmente inaceptables. La recolección de basura en tiempo real se puede realizar pausando el recolector de basura, mientras que la gestión manual de memoria en tiempo real requiere evitar grandes desasignaciones o pausar manualmente la desasignación.

Véase también

Referencias

  1. McCarthy, John; Abrahams, Paul W.; Edwards, Daniel J.; Hart, Timothy P.; Levin, Michael I. (1985) [1962]. Manual del programador de LISP 1.5 (PDF) . 15.ª impresión (2.ª  ed.). p.  Prefacio.
  2. "El creador de C++ pide medidas para abordar los 'ataques graves'"" . Archivado del original el 14-01-2026 . Recuperado el 16-01-2026 .
  3. El equipo de Rust (19 de abril de 2026). "RAII - Rust por ejemplo" . doc.rust-lang.org . El equipo de Rust.
  4. JeanHeyd Meneide (2025). "Programación - C - defer, un mecanismo para deshacer basado en el ámbito léxico de propósito general" (PDF) . open-std.org . WG 14.
  • Berger, ED; Zorn, BG; McKinley, KS (noviembre de 2002). «Reconsiderando la asignación de memoria personalizada» (PDF) . Actas de la 17.ª conferencia ACM SIGPLAN sobre programación orientada a objetos, sistemas, lenguajes y aplicaciones . OOPSLA '02. págs. 1-12 . CiteSeerX 10.1.1.119.5298 . doi : 10.1145/582419.582421 . ISBN   1-58113-471-1.
  • Referencia de gestión de memoria archivada el 13/12/2020 en Wayback Machine .
  • Richard Jones y Rafael Lins, Recolección de basura: algoritmos para la gestión automatizada de memoria dinámica , Wiley and Sons (1996), ISBN 0-471-94148-4