Articulo de referencia

Fuga de memoria

En informática , una fuga de memoria es un tipo de fuga de recursos que ocurre cuando un programa informático gestiona incorrectamente las asignaciones de memoria [ 1 ] de maner...

En informática , una fuga de memoria es un tipo de fuga de recursos que ocurre cuando un programa informático gestiona incorrectamente las asignaciones de memoria [ 1 ] de manera que la memoria que ya no se necesita no se libera. Una fuga de memoria también puede ocurrir cuando un objeto se almacena en la memoria pero no puede ser accedido por el código en ejecución (es decir, memoria inaccesible ) [ 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 se produce cuando un programa consume memoria excesiva pero finalmente la libera. [ 3 ]

Debido a que pueden agotar la memoria del sistema disponible mientras se ejecuta una aplicación, las fugas de memoria suelen ser la causa o un factor que contribuye al envejecimiento del software .

Efectos

Fugas menores

Si un programa tiene una fuga de memoria y su uso de memoria aumenta progresivamente, normalmente no habrá síntomas inmediatos. En los sistemas operativos modernos, la memoria que utiliza una aplicación se libera cuando esta finaliza. Esto significa que una fuga de memoria en un programa que se ejecuta durante un breve periodo de tiempo puede pasar desapercibida y rara vez es grave; además, las fugas lentas pueden ocultarse reiniciando el programa. Todo sistema físico tiene una cantidad finita de memoria, y si la fuga no se controla (por ejemplo, reiniciando el programa que la provoca), acabará causando problemas a los usuarios. [ 4 ]

Paliza

La mayoría de los sistemas operativos modernos para ordenadores de sobremesa de consumo disponen de 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 recibe la cantidad de memoria que solicita. Las páginas activas se transfieren a la memoria principal para un acceso rápido; las páginas inactivas se desplazan al almacenamiento secundario para liberar espacio, según sea necesario. Cuando un solo proceso empieza a consumir una gran cantidad de memoria, suele ocupar cada vez más memoria principal, desplazando a otros programas al almacenamiento secundario  , lo que suele ralentizar significativamente el rendimiento del sistema. Incluso si se finaliza el programa que consume mucha memoria, puede que otros programas tarden un tiempo en volver a la memoria principal y en que el rendimiento se normalice. Esta lentitud y el acceso excesivo al almacenamiento secundario se conocen como saturación de la memoria .

Condición de falta de memoria

Si un programa utiliza toda la memoria disponible antes de finalizar (ya sea memoria virtual o solo memoria principal, como en un sistema embebido), cualquier intento de asignar más memoria fallará. Esto suele provocar que el programa que intenta asignar la memoria finalice o genere un error de segmentación . Algunos programas están diseñados para recuperarse de esta situación (posiblemente recurriendo a la memoria prereservada). El primer programa que experimente el error de falta de memoria puede o no ser el mismo que presenta la fuga de memoria.

Algunos sistemas operativos multitarea cuentan con mecanismos especiales para gestionar la falta de memoria, como la eliminación aleatoria de procesos (lo que puede afectar a procesos "inocentes") o la eliminación del proceso que consume más memoria (que presumiblemente es el causante del problema). Otros sistemas operativos establecen un límite de memoria por proceso para evitar que un solo programa acapare toda la memoria del sistema. La desventaja de esta configuración es que, en ocasiones, es necesario reconfigurar el sistema operativo para permitir el correcto funcionamiento de programas que requieren grandes cantidades de memoria, como los que se encargan de gráficos, vídeo o cálculos científicos.

Si la fuga de memoria se encuentra en el núcleo , es probable que el propio sistema operativo falle. Los ordenadores sin una gestión de memoria sofisticada, como los sistemas embebidos, también pueden fallar por completo debido a una fuga de memoria persistente.

Causas de fugas graves

Entre las filtraciones mucho más graves se incluyen aquellas en las que:

  • Un programa se ejecuta durante mucho tiempo y consume memoria adicional con el tiempo, como las tareas en segundo plano en los servidores, y especialmente en los sistemas embebidos que pueden dejarse en funcionamiento durante muchos años.
  • Se asigna memoria nueva con frecuencia para tareas puntuales, como al renderizar los fotogramas de un videojuego o un vídeo de animación.
  • Un programa puede solicitar memoria, como memoria compartida , que no se libera, incluso cuando el programa finaliza.
  • La memoria es muy limitada, como en un sistema embebido o un dispositivo portátil, o cuando el programa requiere una gran cantidad de memoria desde el principio, lo que deja poco margen para fugas.
  • Se produce una fuga dentro del sistema operativo o del administrador de memoria.
  • Un controlador de dispositivo del sistema provoca una fuga de memoria.
  • El sistema operativo no libera automáticamente la memoria al finalizar un programa.

Problemas de programación

Las fugas de memoria son un error común en la programación, especialmente al usar lenguajes que no tienen recolección de basura automática integrada , como C y C++ . Normalmente, una fuga de memoria ocurre porque la memoria asignada dinámicamente se vuelve inaccesible . La prevalencia de errores de fuga de memoria ha llevado al desarrollo de varias herramientas de depuración para detectar memoria inaccesible. 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 "conservadora" a cualquier lenguaje de programación que carezca de esta característica integrada, y existen bibliotecas para hacerlo para programas C y C++. Un recolector conservador encuentra y recupera la mayor parte, pero no toda, la memoria inaccesible.

Aunque el gestor de memoria puede recuperar memoria inaccesible, no puede liberar memoria que aún sea accesible y, por lo tanto, potencialmente útil. Por consiguiente, los gestores 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 gestor de memoria no libera un objeto que sea fuertemente accesible. Un objeto es fuertemente accesible si se puede acceder a él 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 , impide que un objeto sea recolectado por el recolector de basura). Para evitar esto, el desarrollador es responsable de limpiar las referencias después de su uso, normalmente estableciendo la referencia a nulo una vez que ya no sea necesaria 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 de limpieza, ni por si un objeto sigue referenciado o no. Para un programador, es más fácil saber cuándo ya no se necesita una referencia que cuándo ya no se referencia un objeto. Sin embargo, la gestión automática de memoria puede generar una sobrecarga de rendimiento y no elimina todos los errores de programación que provocan fugas de memoria.

Explotación

Los sistemas de acceso público, como los servidores web o los enrutadores, son vulnerables a ataques de denegación de servicio si un atacante descubre una secuencia de operaciones que pueda provocar una fuga de información. Dicha secuencia se conoce como exploit .

RAII

La adquisición de recursos mediante inicialización (RAII) es un enfoque común para este problema en C++ , D y Ada . Consiste en asociar objetos con ámbito a los recursos adquiridos y liberarlos automáticamente una vez que los objetos quedan fuera de su á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 en C y C++:

Cª:

#include <stdlib.h>void algunaOperación ( int * a ) { // ... }void f ( int n ) { int * a = ( int * ) calloc ( n , sizeof ( int )); someOperation ( a ); free ( a ); }

En C++:

importar std ;usando std :: vector ;void algunaOperación ( vector < int >&a a ) { // ... }void f ( int n ) { vector < int > a ( n ); algunaOperación ( a ); }

La versión en C, tal como se implementa en el ejemplo, requiere una desasignación explícita; el array 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. (Tenga en cuenta que, a diferencia de malloc(), calloc()inicializa todos los elementos a 0).

La versión en C++ no requiere una desasignación explícita; siempre se producirá automáticamente en cuanto el objeto asalga del ámbito, incluso si se lanza una excepción. Esto evita parte de la sobrecarga de los esquemas de recolección de basura . Y dado que los destructores de objetos pueden liberar recursos distintos de la memoria, RAII ayuda a prevenir la fuga de recursos de entrada y salida a los que se accede mediante un identificador , algo que la recolección de basura de marcado y barrido no gestiona correctamente. Esto incluye 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, usar RAII correctamente no siempre es fácil y tiene sus propios inconvenientes. Por ejemplo, si no se tiene cuidado, es posible crear punteros (o referencias) colgantes al devolver datos por referencia, lo que provoca que dichos datos se eliminen cuando el objeto que los contiene queda fuera del ámbito.

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 ámbito original, y la recolección de basura en caso contrario.

Conteo de referencias y referencias cíclicas

Los sistemas de recolección de basura más modernos suelen basarse en la accesibilidad  : si no se dispone de una referencia utilizable a la memoria en cuestión, esta puede ser recolectada. Otros sistemas se basan en el conteo de referencias , donde un objeto es responsable de llevar un registro de cuántas referencias apuntan a él. Si el número llega a cero, se espera que el objeto se libere y permita que se recupere su memoria. El inconveniente de este modelo es que no maneja las referencias cíclicas, y por eso hoy en día la mayoría de los programadores están dispuestos a asumir la carga de los sistemas de marcado y barrido, que son más costosos.

El siguiente código de Visual Basic ilustra la fuga de memoria del conteo de referencias canónico:

Dim A , B Set A = CreateObject ( "Some.Thing" ) Set B = CreateObject ( "Some.Thing" ) ' En este punto, cada uno de los dos objetos tiene una referencia,Conjunto A . miembro = B Conjunto B . miembro = A ' Ahora cada uno tiene dos referencias.Conjunto A = Nada ' Aún podrías salir de esta...Establecer B = Nada ' ¡Y ahora tienes una fuga de memoria!Fin

En la práctica, este ejemplo trivial se detectaría y corregiría de inmediato. En la mayoría de los casos reales, el ciclo de referencias abarca más de dos objetos y resulta más difícil de detectar.

Un ejemplo conocido de este tipo de fuga de memoria cobró relevancia con el auge de las técnicas de programación AJAX en los navegadores web , concretamente con el problema del oyente latente . El código JavaScript que asociaba un elemento DOM con un controlador de eventos y no eliminaba la referencia antes de finalizar, provocaba una fuga de memoria (las páginas web AJAX mantienen un elemento DOM activo durante mucho más tiempo que las páginas web tradicionales, por lo que esta fuga era mucho más evidente).

Detección externa

El patrón de utilización de la memoria en forma de "diente de sierra": la caída repentina en la memoria utilizada es un posible síntoma de una fuga de memoria.

Un patrón de utilización de memoria en forma de sierra puede indicar una fuga de memoria en una aplicación, especialmente si las caídas verticales coinciden con reinicios de dicha aplicación. Sin embargo, conviene tener cuidado, ya que los puntos de recolección de basura también podrían causar este patrón e indicar un uso adecuado del montón.

El uso de memoria en constante aumento no es necesariamente evidencia de una fuga de memoria. Algunas aplicaciones almacenan cantidades cada vez mayores de información en la memoria (por ejemplo, como caché ). Si la caché crece tanto que causa problemas, esto podría deberse a un error de programación o diseño, pero no a una fuga 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 específica; por ejemplo, un procesador de archivos gráficos podría comenzar leyendo todo el contenido de un archivo de imagen y almacenándolo en la memoria, algo que no es viable cuando una imagen muy grande excede la memoria disponible. Para confirmar que el uso excesivo de memoria se debe a una fuga de memoria, es necesario acceder al código del programa.

Ejemplos

Pseudocódigo

El siguiente ejemplo, escrito en pseudocódigo , pretende mostrar cómo puede producirse una fuga de memoria y sus efectos, sin necesidad de conocimientos de programación. El programa en cuestión 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 pulsa el botón de un piso.

Cuando se pulsa un botón: Consigue algo de memoria, que te servirá para recordar el número de piso. Introduce el número de piso en la memoria. ¿Ya estamos en el nivel objetivo? Si es así, no tenemos nada que hacer: terminado. De lo contrario: Espere hasta que el elevador esté parado. Diríjase al piso correspondiente. Libera el recuerdo que usábamos para recordar el número de piso.

La fuga de memoria se produciría si el número de piso solicitado coincide con el piso en el que se encuentra el ascensor; en ese caso, se omitiría la condición para liberar la memoria. Cada vez que esto ocurre, se produce una mayor fuga de memoria.

Casos como este no suelen tener efectos inmediatos. 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 como para que esto ocurriera cientos o miles de veces. Sin embargo, con el tiempo, la memoria del ascensor se agotará. Esto podría tardar meses o años, por lo que podría pasar desapercibido incluso tras realizar pruebas exhaustivas.

Las consecuencias serían desagradables; como mínimo, el ascensor dejaría de responder a las solicitudes para subir a otro piso (por ejemplo, al intentar llamarlo o al pulsar los botones). Si otras partes del programa requieren memoria (una parte para abrir y cerrar la puerta, por ejemplo), nadie podría entrar, y si alguien se encontrara dentro, quedaría atrapado (siempre que las puertas no se puedan abrir manualmente).

La fuga de memoria persiste hasta que se reinicia el sistema. Por ejemplo: si se corta la energía del ascensor o hay un apagón, el programa deja de funcionar. Al restablecerse la energía, el programa se reinicia y toda la memoria vuelve a estar disponible, pero el lento proceso de fuga 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 sacando la operación de "liberación" fuera de la condición:

Cuando se pulsa un botón: Consigue algo de memoria, que te servirá para recordar el número de piso. Introduce el número de piso en la memoria. ¿Ya estamos en el nivel objetivo? Si no: Espere hasta que el elevador esté parado. Diríjase al piso correspondiente. Libera el recuerdo que usábamos para recordar el número de piso.

C++

La siguiente función de C++ provoca una fuga de memoria deliberada al perder el puntero a la memoria asignada.

void causeLeak () { int * a = new int [ 5 ]; a = nullptr ; /**  * El puntero en 'a' ya no existe y, por lo tanto, no se puede liberar,  * pero el sistema aún asigna memoria.  * Si el programa continúa creando dichos punteros sin liberarlos,  * consumirá memoria continuamente.  * Por lo tanto, se produciría una fuga.  * Una eliminación correspondiente debería coincidir con una nueva llamada.  */ }

Véase también

Referencias

  1. Crockford, Douglas. "Fugas de memoria en JScript" . Archivado del original el 7 de diciembre de 2012. Consultado el 20 de julio de 2022 .
  2. "Creando una fuga de memoria con Java" . Stack Overflow . Consultado el 14 de junio de 2013 .
  3. Mitchell, Neil. "Espacio con fugas" . Consultado el 27 de mayo de 2017 .
  4. Rudafshani, Masoomeh y Paul AS Ward. "LeakSpot: Detección y diagnóstico de fugas de memoria en aplicaciones JavaScript". Software, practice & experience 47.1 (2017): 97–123. Web.
  • Detector de fugas visuales Archivado el 15/12/2015 en Wayback Machine para Visual Studio, código abierto
  • Valgrind , código abierto
  • Deleaker para Visual Studio, propietario
  • Validador de memoria para Visual Studio, Delphi, Fortran, Visual Basic, propietario
  • Detección de fugas de memoria (mediante la compatibilidad con la depuración de MFC)
  • Artículo " Detección de fugas de memoria en sistemas embebidos " de Cal Erickson
  • WonderLeak , un perfilador de alto rendimiento para la asignación de memoria y manejadores en Windows, es de propiedad exclusiva.