En informática , una pérdida de memoria es un tipo de pérdida de recursos que ocurre cuando un programa de computadora administra incorrectamente las asignaciones de memoria [1] de manera que la memoria que ya no es necesaria no se libera. También puede ocurrir una pérdida de memoria cuando un objeto está almacenado en la memoria pero el código en ejecución no puede acceder a él (es decir, memoria inalcanzable ). [2] Una pérdida de memoria tiene síntomas similares a otros problemas y generalmente solo puede ser diagnosticada por un programador con acceso al código fuente del programa.
Un concepto relacionado es la "fuga de espacio", que ocurre cuando un programa consume memoria excesiva pero finalmente la libera. [3]
Debido a que pueden agotar la memoria disponible del sistema cuando se ejecuta una aplicación, las pérdidas de memoria suelen ser la causa o un factor que contribuye al envejecimiento del software .
Una pérdida de memoria reduce el rendimiento de la computadora al reducir la cantidad de memoria disponible. Una pérdida de memoria puede provocar un aumento en el uso de la memoria, el tiempo de ejecución del rendimiento y puede afectar negativamente a la experiencia del usuario. [4] Con el tiempo, en el peor de los casos, se puede asignar demasiada memoria disponible y todo o parte del sistema o dispositivo deja de funcionar correctamente, la aplicación falla o el sistema se ralentiza enormemente debido a la paliza .
Es posible que las pérdidas de memoria no sean graves o ni siquiera detectables por medios normales. En los sistemas operativos modernos, la memoria normal utilizada por una aplicación se libera cuando la aplicación finaliza. Esto significa que una pérdida de memoria en un programa que sólo se ejecuta durante un breve periodo de tiempo puede no ser detectada y rara vez es grave.
Las filtraciones mucho más graves incluyen aquellas:
El siguiente ejemplo, escrito en pseudocódigo , pretende mostrar cómo se puede producir una pérdida de memoria y sus efectos, sin necesidad de conocimientos de programación. El programa en este caso forma parte de un software muy sencillo diseñado para controlar un ascensor . Esta parte del programa se ejecuta cada vez que alguien dentro del ascensor presiona el botón de un piso.
Cuando se presiona un botón: Obtenga algo de memoria, que se utilizará para recordar el número del piso. Pon el número del piso en la memoria. ¿Estamos ya en el piso objetivo? Si es así, no tenemos nada que hacer: terminado De lo contrario: Espere hasta que el ascensor esté inactivo Ir al piso requerido Libera la memoria que usamos para recordar el número del piso.
La pérdida de memoria ocurriría si el número de piso solicitado es el mismo piso en el que se encuentra el ascensor; se omitiría la condición para liberar la memoria. Cada vez que ocurre este caso, se pierde más memoria.
Casos como este normalmente no tendrían efectos inmediatos. Las personas no suelen presionar el botón del piso en el que ya se encuentran y, en cualquier caso, el ascensor puede tener suficiente memoria libre para que esto pueda suceder cientos o miles de veces. Sin embargo, el ascensor eventualmente se quedará sin memoria. Esto podría llevar meses o años, por lo que es posible que no se descubra a pesar de realizar pruebas exhaustivas.
Las consecuencias serían desagradables; como mínimo, el ascensor dejaría de responder a las solicitudes de pasar a otro piso (como cuando se intenta llamar al ascensor o cuando alguien está dentro y presiona los botones del piso). Si otras partes del programa necesitan memoria (una parte asignada para abrir y cerrar la puerta, por ejemplo), entonces nadie podrá entrar, y si alguien está dentro, quedará atrapado (suponiendo que las puertas no puedan abrirse). abierto manualmente).
La pérdida de memoria dura hasta que se reinicia el sistema. Por ejemplo: si se cortara la energía del ascensor o se produjera un corte de energía, el programa dejaría de ejecutarse. Cuando se volvía a encender, el programa se reiniciaba y toda la memoria volvía a estar disponible, pero el lento proceso de pérdida de memoria se reiniciaba junto con el programa, perjudicando finalmente el correcto funcionamiento del sistema.
La fuga en el ejemplo anterior se puede corregir llevando la operación de "liberación" fuera del condicional:
Cuando se presiona un botón: Obtenga algo de memoria, que se utilizará para recordar el número del piso. Pon el número del piso en la memoria. ¿Estamos ya en el piso objetivo? Si no: Espere hasta que el ascensor esté inactivo Ir al piso requerido Libera la memoria que usamos para recordar el número del piso.
Las pérdidas de memoria son un error común en la programación, especialmente cuando se utilizan lenguajes que no tienen recolección automática de basura incorporada , como C y C++ . Normalmente, se produce una pérdida de memoria porque la memoria asignada dinámicamente se ha vuelto inalcanzable . La prevalencia de errores de pérdida de memoria ha llevado al desarrollo de una serie de herramientas de depuración para detectar memoria inalcanzable. BoundsChecker , Deleaker , Memory Validator, IBM Rational Purify , Valgrind , Parasoft Insure++ , Dr. Memory y memwatch son algunos de los depuradores de memoria más populares para programas C y C++. Se pueden agregar capacidades "conservadoras" de recolección de basura a cualquier lenguaje de programación que carezca de ellas como característica incorporada, y hay bibliotecas para hacer esto disponibles para programas C y C++. Un coleccionista conservador encuentra y recupera la mayoría, pero no toda, la memoria inalcanzable.
Aunque el administrador de memoria puede recuperar memoria inalcanzable, no puede liberar memoria que todavía es accesible y, por lo tanto, potencialmente útil. Por lo tanto, los administradores de memoria modernos proporcionan técnicas para que los programadores marquen semánticamente la memoria con distintos niveles de utilidad, que corresponden a distintos niveles de accesibilidad . El administrador de memoria no libera un objeto al que se puede acceder fácilmente. Un objeto es fuertemente alcanzable si es accesible directamente mediante una referencia fuerte o indirectamente mediante una cadena de referencias fuertes. (Una referencia fuerte es una referencia que, a diferencia de una referencia débil , evita que un objeto sea recolectado como basura). Para evitar esto, el desarrollador es responsable de limpiar las referencias después de su uso, generalmente estableciendo la referencia en nula una vez que ya no lo es. necesario y, si es necesario, cancelando el registro de cualquier detector de eventos que mantenga fuertes referencias al objeto.
En general, la gestión automática de la memoria es más sólida y conveniente para los desarrolladores, ya que no necesitan implementar rutinas de liberación ni preocuparse por la secuencia en la que se realiza la limpieza ni preocuparse por si todavía se hace referencia a un objeto o no. Es más fácil para un programador saber cuándo ya no se necesita una referencia que saber cuándo ya no se hace referencia a un objeto. Sin embargo, la administración automática de la memoria puede imponer una sobrecarga de rendimiento y no elimina todos los errores de programación que causan pérdidas de memoria.
La adquisición de recursos es inicialización (RAII) es un enfoque al problema comúnmente adoptado en C++ , D y Ada . Implica asociar objetos con alcance con los recursos adquiridos y liberar automáticamente los recursos una vez que los objetos estén fuera del alcance. A diferencia de la recolección de basura, RAII tiene la ventaja de saber cuándo existen los objetos y cuándo no. Compare los siguientes ejemplos de C y C++:
/* Versión C */ #include <stdlib.h> void f ( int n ) { int * matriz = calloc ( n , tamaño de ( int )); hacer_algo_de_trabajo ( matriz ); gratis ( matriz ); }
// versión C++ #include <vector> void f ( int n ) { std :: vector <int> matriz ( n ) ; hacer_algo_de_trabajo ( matriz ); }
La versión C, tal como se implementa en el ejemplo, requiere una desasignación explícita; la matriz se asigna dinámicamente (desde el montón en la mayoría de las implementaciones de C) y continúa existiendo hasta que se libera explícitamente.
La versión C++ no requiere una desasignación explícita; siempre ocurrirá automáticamente tan pronto como el objeto array
salga del alcance, incluso si se lanza una excepción. Esto evita algunos de los gastos generales de los sistemas de recogida de basura . Y debido a que los destructores de objetos pueden liberar recursos distintos de la memoria, RAII ayuda a evitar la fuga de recursos de entrada y salida a los que se accede a través de un identificador , que la recolección de basura de marca y barrido no maneja correctamente. Estos incluyen archivos abiertos, ventanas abiertas, notificaciones de usuario, objetos en una biblioteca de dibujo de gráficos, primitivas de sincronización de subprocesos, como secciones críticas, conexiones de red y conexiones al Registro de Windows u otra base de datos.
Sin embargo, utilizar RAII correctamente no siempre es fácil y tiene sus propios inconvenientes. Por ejemplo, si uno no tiene cuidado, es posible crear punteros (o referencias) colgantes devolviendo datos por referencia, solo para eliminar esos datos cuando el objeto que los contiene sale del alcance.
D utiliza una combinación de RAII y recolección de basura, empleando destrucción automática cuando está claro que no se puede acceder a un objeto fuera de su alcance original, y recolección de basura en caso contrario.
Los esquemas de recolección de basura más modernos a menudo se basan en una noción de accesibilidad: si no se tiene una referencia utilizable a la memoria en cuestión, se puede recolectar. Otros esquemas de recolección de basura pueden basarse en el recuento de referencias , donde un objeto es responsable de realizar un seguimiento de cuántas referencias apuntan a él. Si el número baja a cero, se espera que el objeto se libere y permita recuperar su memoria. El defecto de este modelo es que no soporta referencias cíclicas, y es por eso que hoy en día la mayoría de los programadores están preparados para aceptar la carga de los sistemas más costosos de marca y barrido .
El siguiente código de Visual Basic ilustra la pérdida de memoria canónica del recuento de referencias:
Dim A , B Set A = CreateObject ( "Some.Thing" ) Set B = CreateObject ( "Some.Thing" ) ' En este punto, los dos objetos tienen cada uno una referencia, Establecer A. miembro = B Conjunto B . miembro = A ' Ahora cada uno tiene dos referencias. Conjunto A = Nada ' Aún podrías salir de él... Conjunto B = Nada ' ¡Y ahora tienes una pérdida de memoria! Fin
En la práctica, este ejemplo trivial se detectaría de inmediato y se solucionaría. En la mayoría de los ejemplos reales, el ciclo de referencias abarca más de dos objetos y es más difícil de detectar.
Un ejemplo bien conocido de este tipo de filtración saltó a la fama con el surgimiento de las técnicas de programación AJAX en los navegadores web en el problema del oyente caducado . El código JavaScript que asociaba un elemento DOM con un controlador de eventos y no eliminaba la referencia antes de salir, perdería memoria (las páginas web AJAX mantienen vivo un DOM determinado durante mucho más tiempo que las páginas web tradicionales, por lo que esta fuga fue mucho más evidente) .
Si un programa tiene una pérdida de memoria y su uso aumenta constantemente, normalmente no habrá un síntoma inmediato. Cada sistema físico tiene una cantidad finita de memoria, y si la pérdida de memoria no se contiene (por ejemplo, reiniciando el programa con fugas), eventualmente causará problemas.
La mayoría de los sistemas operativos de escritorio de consumo modernos tienen memoria principal , alojada físicamente en microchips de RAM, y almacenamiento secundario , como un disco duro . La asignación de memoria es dinámica: cada proceso obtiene tanta memoria como solicita. Las páginas activas se transfieren a la memoria principal para un acceso rápido; las páginas inactivas se envían al almacenamiento secundario para hacer espacio, según sea necesario. Cuando un único proceso comienza a consumir una gran cantidad de memoria, normalmente ocupa cada vez más memoria principal, empujando a otros programas al almacenamiento secundario, lo que normalmente ralentiza significativamente el rendimiento del sistema. Incluso si se finaliza el programa con fugas, es posible que otros programas tarden algún tiempo en volver a la memoria principal y que el rendimiento vuelva a la normalidad.
Cuando se agota toda la memoria de un sistema (ya sea que haya memoria virtual o solo memoria principal, como en un sistema integrado), cualquier intento de asignar más memoria fallará. Esto generalmente hace que el programa que intenta asignar la memoria termine por sí solo o genere un error de segmentación . Algunos programas están diseñados para recuperarse de esta situación (posiblemente recurriendo a la memoria previamente reservada). El primer programa que experimente falta de memoria puede ser o no el programa que tiene la pérdida de memoria.
Algunos sistemas operativos multitarea tienen mecanismos especiales para lidiar con una condición de falta de memoria, como matar procesos al azar (lo que puede afectar procesos "inocentes") o matar el proceso más grande en la memoria (que presumiblemente es el que causa el problema). Algunos sistemas operativos tienen un límite de memoria por proceso, para evitar que un programa acapare toda la memoria del sistema. La desventaja de esta disposición es que a veces es necesario reconfigurar el sistema operativo para permitir el funcionamiento adecuado de programas que legítimamente requieren grandes cantidades de memoria, como los que se ocupan de gráficos, vídeos o cálculos científicos.
Si la pérdida de memoria está en el kernel , es probable que el sistema operativo falle. Las computadoras sin una administración de memoria sofisticada, como los sistemas integrados, también pueden fallar por completo debido a una pérdida de memoria persistente.
Los sistemas de acceso público, como servidores web o enrutadores, son propensos a sufrir ataques de denegación de servicio si un atacante descubre una secuencia de operaciones que puede desencadenar una fuga. Esta secuencia se conoce como exploit .
Un patrón de utilización de memoria en forma de "dientes de sierra" puede ser un indicador de una pérdida de memoria dentro de una aplicación, particularmente si las caídas verticales coinciden con reinicios o reinicios de esa aplicación. Sin embargo, se debe tener cuidado porque los puntos de recolección de basura también podrían causar ese patrón y mostrarían un uso saludable del montón.
Tenga en cuenta que el uso de memoria en constante aumento no es necesariamente evidencia de una pérdida de memoria. Algunas aplicaciones almacenarán cantidades cada vez mayores de información en la memoria (por ejemplo, como caché ). Si el caché puede crecer tanto como para causar problemas, esto puede ser un error de programación o diseño, pero no es una pérdida de memoria ya que la información permanece nominalmente en uso. En otros casos, los programas pueden requerir una cantidad excesiva de memoria porque el programador ha asumido que la memoria siempre es suficiente para una tarea particular; por ejemplo, un procesador de archivos gráficos podría comenzar leyendo todo el contenido de un archivo de imagen y almacenándolo todo en la memoria, algo que no es viable cuando una imagen muy grande excede la memoria disponible.
Para decirlo de otra manera, una pérdida de memoria surge de un tipo particular de error de programación y, sin acceso al código del programa, alguien que vea los síntomas sólo puede adivinar que podría haber una pérdida de memoria. Sería mejor utilizar términos como "uso de memoria en constante aumento" cuando no exista tal conocimiento interno.
El siguiente programa C++ pierde memoria deliberadamente al perder el puntero a la memoria asignada.
int principal () { int * a = nuevo int ( 5 ); a = nulo ; /* El puntero en 'a' ya no existe y, por lo tanto, no se puede liberar, pero el sistema aún asigna la memoria. Si el programa continúa creando dichos punteros sin liberarlos, consumirá memoria continuamente. Por tanto, se produciría una fuga. */ }