En informática , una fuga de memoria es un tipo de fuga de recursos que se produce cuando un programa informático gestiona incorrectamente las asignaciones de memoria [1] de forma que no se libera la memoria que ya no se necesita. Una fuga de memoria también puede ocurrir cuando un objeto se almacena en la memoria pero el código en ejecución no puede acceder a él (es decir, memoria inalcanzable ). [2] Una fuga de memoria tiene síntomas similares a los de otros problemas y, por lo general, 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 mientras se ejecuta una aplicación, las fugas 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 la experiencia del usuario. [4] Finalmente, en el peor de los casos, es posible que se asigne demasiada memoria disponible y todo o parte del sistema o dispositivo deje de funcionar correctamente, la aplicación falle o el sistema se ralentice enormemente debido a la sobrecarga .
Las fugas de memoria pueden no ser graves o incluso no ser detectables por medios normales. En los sistemas operativos modernos, la memoria normal utilizada por una aplicación se libera cuando esta finaliza. Esto significa que una fuga de memoria en un programa que solo se ejecuta durante un breve período de tiempo puede pasar desapercibida y rara vez es grave.
Entre las filtraciones mucho más graves se incluyen las siguientes:
El siguiente ejemplo, escrito en pseudocódigo , pretende mostrar cómo se puede producir una fuga 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 siempre que alguien dentro del ascensor pulse el botón de un piso.
Cuando se presiona un botón: Consigue algo de memoria, que se utilizará para recordar el número de piso. Ponga el número del piso en la memoria ¿Estamos ya en el piso objetivo? Si es así no tenemos nada que hacer: se acabó. 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 se produciría si el número de piso solicitado fuera el mismo piso en el que se encuentra el ascensor; se omitiría la condición para liberar la memoria. Cada vez que se produce este caso, se pierde más memoria.
Casos como este no suelen tener consecuencias inmediatas. La gente no suele pulsar el botón del piso en el que ya se encuentra y, en cualquier caso, el ascensor podría tener suficiente memoria libre como para que esto ocurra cientos o miles de veces. Sin embargo, el ascensor acabará quedándose 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 peticiones de pasar a otro piso (como cuando se intenta llamar al ascensor o cuando alguien está dentro y pulsa los botones del piso). Si otras partes del programa necesitan memoria (una parte asignada a abrir y cerrar la puerta, por ejemplo), entonces nadie podría entrar, y si alguien está dentro, quedará atrapado (suponiendo que las puertas no se puedan abrir manualmente).
La pérdida de memoria dura hasta que se reinicia el sistema. Por ejemplo: si se corta la energía del ascensor o se produce un corte de energía, el programa deja de funcionar. Al volver a encenderse, el programa se reinicia y toda la memoria vuelve a estar disponible, pero el lento proceso de pérdida de memoria se reinicia junto con el programa, lo que acaba perjudicando el correcto funcionamiento del sistema.
La fuga en el ejemplo anterior se puede corregir llevando la operación "liberación" fuera del condicional:
Cuando se presiona un botón: Consigue algo de memoria, que se utilizará para recordar el número de piso. Ponga 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 fugas de memoria son un error común en programación, especialmente cuando se utilizan lenguajes que no tienen una recolección de basura automática incorporada , como C y C++ . Normalmente, una fuga de memoria ocurre porque la memoria asignada dinámicamente se ha vuelto inalcanzable . La prevalencia de errores de fuga 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 de recolección de basura "conservadoras" a cualquier lenguaje de programación que carezca de ellas como una característica incorporada, y las bibliotecas para hacer esto están disponibles para programas C y C++. Un recolector 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 aún es alcanzable y, por lo tanto, potencialmente aún útil. Por lo tanto, los administradores de memoria modernos proporcionan técnicas para que los programadores marquen semánticamente la memoria con diferentes niveles de utilidad, que corresponden a diferentes niveles de accesibilidad . El administrador de memoria no libera un objeto que es fuertemente alcanzable. Un objeto es fuertemente alcanzable si es alcanzable ya sea directamente por una referencia fuerte o indirectamente por 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 del uso, generalmente estableciendo la referencia en null una vez que ya no se necesita y, si es necesario, anulando el registro de cualquier detector de eventos que mantenga referencias fuertes al objeto.
En general, la gestión automática de memoria es más robusta 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 por si un objeto sigue siendo referenciado o no. Es más fácil para un programador saber cuándo ya no se necesita una referencia que cuándo ya no se hace referencia a un objeto. Sin embargo, la gestión automática de memoria puede suponer una sobrecarga de rendimiento y no elimina todos los errores de programación que provocan fugas de memoria.
La adquisición de recursos es la inicialización (RAII) es un enfoque del problema que se utiliza comúnmente en C++ , D y Ada . Implica asociar objetos dentro del ámbito con los recursos adquiridos y liberar automáticamente los recursos una vez que los objetos están fuera del ámbito. 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 , sizeof ( int )); hacer_algo_de_trabajo ( matriz ); libre ( matriz ); }
// Versión de 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 se producirá automáticamente tan pronto como el objeto array
salga del ámbito, incluso si se lanza una excepción. Esto evita parte de la sobrecarga de los esquemas de recolección 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 marcado y barrido no maneja correctamente. Estos incluyen archivos abiertos, ventanas abiertas, notificaciones de usuario, objetos en una biblioteca de dibujo gráfico, 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, el uso correcto de RAII no siempre es fácil y tiene sus propios inconvenientes. Por ejemplo, si uno no tiene cuidado, es posible crear punteros colgantes (o referencias) al devolver datos por referencia, solo para que esos datos se eliminen cuando el objeto que los contiene quede fuera del alcance.
D utiliza una combinación de RAII y recolección de basura, empleando la destrucción automática cuando está claro que no se puede acceder a un objeto fuera de su alcance original, y la recolección de basura en caso contrario.
Los esquemas de recolección de basura más modernos suelen basarse 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 conteo 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 que se recupere su memoria. El defecto de este modelo es que no se ocupa de las 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 de tipo marcado y barrido, que son más costosos .
El siguiente código de Visual Basic ilustra la pérdida de memoria de conteo de referencias canónicas:
Dim A , B Set A = CreateObject ( "Some.Thing" ) Set B = CreateObject ( "Some.Thing" ) ' En este punto, los dos objetos tienen cada uno una referencia, Conjunto A . miembro = B Conjunto B . miembro = A ' Ahora cada uno tiene dos referencias. Conjunto A = Nada ' Aún puedes salir de esto... Conjunto B = Nada ' ¡Y ahora tienes una pérdida de memoria! Fin
En la práctica, este ejemplo trivial se detectaría inmediatamente 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 fuga se hizo evidente con el auge 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, perdí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 era mucho más evidente).
Si un programa tiene una pérdida de memoria y su uso de memoria aumenta de forma constante, no suele haber ningún 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 pérdida), acabará provocando problemas.
La mayoría de los sistemas operativos de escritorio modernos para el consumidor tienen memoria principal , que se encuentra 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 solo proceso comienza a consumir una gran cantidad de memoria, generalmente ocupa cada vez más memoria principal, lo que envía a otros programas al almacenamiento secundario, lo que generalmente reduce significativamente el rendimiento del sistema. Incluso si se finaliza el programa que pierde memoria, puede llevar algún tiempo hasta que otros programas vuelvan a la memoria principal y 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 se termine a sí mismo o genere un error de segmentación . Algunos programas están diseñados para recuperarse de esta situación (posiblemente recurriendo a la memoria reservada previamente). El primer programa que experimente la falta de memoria puede o no ser 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 a 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 correcto de programas que legítimamente requieren grandes cantidades de memoria, como los que se ocupan de gráficos, video o cálculos científicos.
Si la pérdida de memoria se encuentra en el núcleo , es probable que el propio sistema operativo falle. Los equipos que no cuentan con una gestió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 los servidores web o los enrutadores, son propensos a sufrir ataques de denegación de servicio si un atacante descubre una secuencia de operaciones que puede provocar una fuga de información. Dicha secuencia se conoce como exploit .
Un patrón de uso de memoria en "dientes de sierra" puede ser un indicador de una pérdida de memoria dentro de una aplicación, en particular si las caídas verticales coinciden con reinicios de esa aplicación. Sin embargo, se debe tener cuidado porque los puntos de recolección de basura también podrían causar un patrón de este tipo 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 fuga de memoria. Algunas aplicaciones almacenarán cantidades cada vez mayores de información en la memoria (por ejemplo, como caché ). Si la caché puede crecer tanto como para causar problemas, esto puede ser un error de programación o diseño, pero no es una fuga de memoria ya que la información permanece nominalmente en uso. En otros casos, los programas pueden requerir una cantidad irrazonablemente grande 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 puede 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.
En otras palabras, una fuga de memoria surge de un tipo particular de error de programación y, sin acceso al código del programa, alguien que observe los síntomas solo puede adivinar que podría haber una fuga de memoria. Sería mejor utilizar términos como "uso de memoria en constante aumento" cuando no existe tal conocimiento interno.
El siguiente programa C++ pierde memoria deliberadamente al perder el puntero a la memoria asignada.
int main () { int * a = new int ( 5 ); a = nullptr ; /* El puntero en 'a' ya no existe, y por lo tanto no se puede liberar, pero la memoria aún está asignada por el sistema. Si el programa continúa creando dichos punteros sin liberarlos, consumirá memoria continuamente. Por lo tanto, se produciría una fuga. */ }