Compartir a través de


Fundamentos de la recolección de basura

En el Common Language Runtime (CLR), el recolector de elementos no utilizados (GC) actúa como administrador de memoria automático. El recolector de basura administra la asignación y liberación de la memoria de una aplicación. Por lo tanto, los desarrolladores que trabajan con código administrado no tienen que escribir código para realizar tareas de administración de memoria. La administración automática de la memoria puede eliminar problemas frecuentes, como olvidar liberar un objeto y causar una pérdida de memoria, o intentar acceder a la memoria de un objeto que ya se ha liberado.

En este artículo se describen los conceptos básicos de la recolección de elementos no utilizados.

Ventajas

El recolector de basura proporciona las siguientes ventajas:

  • Exime a los desarrolladores de tener que liberar memoria manualmente.

  • Asigna con eficacia los objetos del montón administrado.

  • Reclama los objetos que ya no se utilizan, borra la memoria correspondiente y mantiene la memoria disponible para asignaciones futuras. Los objetos administrados obtienen automáticamente contenido limpio desde el principio, de modo que sus constructores no tienen que inicializar todos los campos de datos.

  • Proporciona seguridad de memoria garantizando que un objeto no pueda utilizar la memoria asignada a otro objeto.

Fundamentos de memoria

En la siguiente lista se resumen los conceptos importantes de memoria de CLR:

  • Cada proceso tiene propio espacio de direcciones virtuales independiente. Todos los procesos del equipo comparten la misma memoria física y el archivo de paginación, si hay alguno.

  • De forma predeterminada, en los equipos de 32 bits, cada proceso tiene un espacio de direcciones virtuales en modo usuario de 2 GB.

  • Como desarrollador de aplicaciones, solo trabaja con el espacio de direcciones virtuales y nunca manipula la memoria física directamente. El recolector de basura asigna y libera memoria virtual en el montón administrado.

    Si está escribiendo código nativo, use funciones de Windows para trabajar con el espacio de direcciones virtuales. Estas funciones asignan y liberan memoria virtual en pilas nativas.

  • La memoria virtual puede estar en tres estados:

    Estado Descripción
    Gratuito El bloque de memoria no tiene ninguna referencia a ella y está disponible para su asignación.
    Reservada El bloque de memoria está disponible para su uso y no se puede emplear para ninguna otra solicitud de asignación. Sin embargo, no puede almacenar datos en este bloque de memoria hasta que esté comprometido.
    Comprometido El bloque de memoria se asigna al almacenamiento físico.
  • El espacio de direcciones virtual se puede fragmentar, lo que significa que hay bloques libres conocidos como agujeros en el espacio de direcciones. Cuando se solicita una asignación de memoria virtual, el administrador de memoria virtual tiene que encontrar un único bloque libre que sea suficientemente grande para satisfacer la solicitud de asignación. Aunque tenga 2 GB de espacio disponible, una asignación que necesite 2 GB no tendrá éxito a menos que todo ese espacio disponible esté en un bloque de direcciones único.

  • Puede quedarse sin memoria si no hay suficiente espacio de direcciones virtuales para reservar o espacio físico para confirmar.

    El archivo de paginación se usa aunque haya poca necesidad de memoria física (demanda de memoria física). La primera vez que la presión de la memoria física es alta, el sistema operativo debe hacer espacio en la memoria física para almacenar los datos y respalda algunos de los datos que están en la memoria física en el archivo de paginación. Los datos no se paginan hasta que se necesitan, por lo que es posible encontrar paginación en situaciones con poca presión de memoria física.

Asignación de memoria

Cuando se inicializa un nuevo proceso, en tiempo de ejecución se reserva una región contigua de espacio de direcciones para el proceso. Este espacio de direcciones reservado se denomina montón administrado. El montón administrado mantiene un puntero a la dirección a la que se asignará el siguiente objeto del montón. Inicialmente, este puntero se establece en la dirección base del montón de memoria administrado. Todos los tipos de referencia se asignan en el montón administrado. Cuando una aplicación crea el primer tipo de referencia, se asigna memoria para el tipo en la dirección base del heap administrado. Cuando la aplicación crea el siguiente objeto, el entorno en tiempo de ejecución asigna memoria para él en el espacio de direcciones que sigue inmediatamente al primer objeto. Siempre que haya espacio de direcciones disponible, el entorno en tiempo de ejecución continúa asignando espacio a los objetos nuevos de este modo.

La asignación de memoria desde el montón administrado es más rápida que la asignación de memoria no administrada. Como el tiempo de ejecución asigna memoria a los objetos agregando un valor a un puntero, este método es casi tan rápido como la asignación de memoria desde la pila. Además, puesto que los nuevos objetos que se asignan consecutivamente se almacenan uno junto a otro en el montón administrado, la aplicación puede acceder rápidamente a los objetos.

Liberación de memoria

El motor de optimización del recolector de basura determina el mejor momento para realizar una recolección basándose en las asignaciones realizadas. Cuando el recolector de elementos no utilizados lleva a cabo una recolección, libera la memoria de los objetos que ya no usa la aplicación. Determina qué objetos ya no se usan examinando las raíces de la aplicación. Las raíces de una aplicación incluyen campos estáticos, variables locales en la pila de un subproceso, registros de la CPU, identificadores de GC y la cola de finalización. Cada raíz hace referencia a un objeto del montón administrado, o bien se establece en null. El recolector de basura puede solicitar estas raíces al resto del tiempo de ejecución. El recolector de basura utiliza esta lista para crear un grafo que contiene todos los objetos que se pueden alcanzar desde las raíces.

Los objetos que no están en el gráfico no se pueden alcanzar desde las raíces de la aplicación. El recolector de basura considera basura a los objetos inaccesibles y libera la memoria asignada para ellos. Durante una recolección, el recolector de elementos no utilizados examina el montón administrado y busca los bloques de espacio de direcciones que ocupan los objetos que no se pueden alcanzar. Cuando detecta cada uno de los objetos inalcanzables, usa una función de copia de memoria para compactar los objetos alcanzables en la memoria y libera los bloques de espacios de direcciones asignados a los objetos no alcanzables. Una vez que se ha compactado la memoria de los objetos alcanzables, el recolector de basura realiza los ajustes necesarios en los punteros para que las raíces de la aplicación apunten a los objetos en sus nuevas ubicaciones. El puntero del montón administrado también se sitúa después del último objeto alcanzable.

La memoria solo se compacta si, durante una recolección, se detecta un número significativo de objetos inalcanzables. Si todos los objetos del montón administrado sobreviven a una recolección, no hay necesidad de compresión de memoria.

Para mejorar el rendimiento, el tiempo de ejecución asigna memoria a los objetos grandes en un montón aparte. El recolector de basura libera automáticamente la memoria para los objetos grandes. Pero, para no mover objetos grandes en la memoria, normalmente dicha memoria no se compacta.

Condiciones para la recolección de basura

La recolección de basura ocurre cuando una de las siguientes condiciones es verdadera:

  • El sistema tiene poca memoria física. El tamaño de la memoria se detecta por la notificación de memoria insuficiente del sistema operativo o cuando el host indica memoria insuficiente.

  • La memoria que utilizan los objetos asignados del montón administrado supera un umbral aceptable. Este umbral se ajusta continuamente a medida que se ejecuta el proceso.

  • Se llama al método GC.Collect . En casi todos los casos, no es necesario llamar a este método, porque el recolector de basura se ejecuta continuamente. Este método se utiliza principalmente para pruebas y situaciones singulares.

Montón administrado

Una vez que el CLR inicializa el recolector de elementos no utilizados, asigna un segmento de memoria para almacenar y administrar objetos. Esta memoria se denomina montón administrado, y se diferencia del montón nativo del sistema operativo.

Hay un montón administrado para cada proceso administrado. Todos los subprocesos del proceso asignan memoria a los objetos del mismo montón.

Para reservar memoria, el recolector de elementos no utilizados llama a la función VirtualAlloc de Windows y reserva un segmento de memoria cada vez para las aplicaciones administradas. El recolector de basura también reserva segmentos cuando sea necesario y los libera de nuevo al sistema operativo (después de eliminar todos los objetos) mediante una llamada a la función VirtualFree de Windows.

Importante

El tamaño de los segmentos asignados por el recolector de basura es específico de la implementación y está sujeto a cambios en cualquier momento, incluidas las actualizaciones periódicas. La aplicación nunca debe realizar suposiciones sobre el tamaño de un sector determinado ni depender de él, y tampoco debe intentar configurar la cantidad de memoria disponible para las asignaciones de segmentos.

Cuantos menos objetos se asignen al montón, menos trabajo tendrá que hacer el recolector de basura. Al asignar objetos, no use valores redondeados que superen sus necesidades; por ejemplo, no asigne una matriz de 32 bytes si solo necesita 15 bytes.

Cuando se desencadena una recolección de basura, el recolector de basura reclama la memoria ocupada por objetos muertos. El proceso de reclamación compacta los objetos activos, moviéndolos juntos, y elimina el espacio muerto, reduciendo así el montículo. Este proceso garantiza que los objetos que se asignan juntos permanezcan juntos en el montón administrado, a fin de conservar su situación.

La intrusividad (frecuencia y duración) de las recolecciones de basura es el resultado del volumen de asignaciones y la cantidad de memoria sobrevivida en el montón administrado.

El montón considerarse una acumulación de dos montones: el montón de objetos grandes y el montón de objetos pequeños. El montón de objetos grandes contiene objetos de 85 000 bytes o más, que normalmente son matrices. Es raro que un objeto de instancia sea extraordinariamente grande.

Sugerencia

Puede configurar el tamaño de umbral para que los objetos se dirijan al montón de objetos grandes.

Generaciones

El algoritmo de GC se basa en varias consideraciones:

  • Es más rápido compactar la memoria de una parte del montón administrado que la de todo el montón.
  • Los objetos más recientes tienen una duración más corta y los objetos antiguos tienen una duración más larga.
  • Los objetos más recientes suelen estar relacionados unos con otros y la aplicación accede a ellos más o menos al mismo tiempo.

La recolección de elementos no utilizados se produce principalmente con la reclamación de objetos de corta duración. Para optimizar el rendimiento del recolector de basura, el montón administrado se divide en tres generaciones: 0, 1 y 2, de forma que pueda manipular los objetos de larga duración y los de corta duración por separado. El recolector de basura almacena los nuevos objetos en la generación 0. Los objetos creados en las primeras etapas de la duración de la aplicación y que sobreviven a las recolecciones se promueven y se almacenan en las generaciones 1 y 2. Como es más rápido compactar una parte del montón administrado que todo el montón, este esquema permite que el recolector de basura libere la memoria de una generación específica en lugar de liberar la memoria de todo el montón administrado cada vez que realiza una recolección.

  • Generación 0: es la más joven y contiene los objetos de corta duración. Un ejemplo de objeto de corta duración es una variable temporal. La recolección de basura se produce con mayor frecuencia en esta generación.

    Los objetos recién asignados constituyen una nueva generación de objetos y son colecciones de la generación 0, implícitamente. Pero si son objetos de gran tamaño, pasan al montón de objetos grandes (LOH), al que en ocasiones se denomina generación 3. La generación 3 es una generación física que se recopila de forma lógica como elemento de la generación 2.

    La mayoría de los objetos son recuperados para la recolección de basura en la generación 0 y no sobreviven a la siguiente generación.

    Si una aplicación intenta crear un nuevo objeto cuando la generación 0 está llena, el recolector de basura realiza una recolección para liberar espacio de memoria para el objeto. El recolector de basura primero examina los objetos en la generación 0 en lugar de todos los objetos en el montón administrado. Una recolección de tan sólo la generación 0 a menudo recupera suficiente memoria para que la aplicación pueda continuar creando objetos.

  • Generación 1: esta generación contiene objetos de corta duración y sirve como búfer entre los objetos de corta y larga duración.

    Una vez que el recolector de elementos no utilizados realiza una recolección de la generación 0, compacta la memoria de los objetos que se pueden alcanzar y los promueve a la generación 1. Dado que los objetos que sobreviven a las recolecciones suelen tener una duración más larga, es lógico promoverlos a una generación superior. El recolector de elementos no utilizados no tiene que volver a examinar los objetos de las generaciones 1 y 2 cada vez que realiza una recolección en la generación 0.

    Si una recolección de la generación 0 no recupera memoria suficiente para que la aplicación pueda crear un nuevo objeto, el recolector de elementos no utilizados puede realizar una recolección de la generación 1 y, después, de la generación 2. Los objetos de la generación 1 que sobreviven a las recolecciones se promueven a la generación 2.

  • Generación 2: esta generación contiene los objetos de larga duración. Un ejemplo de objeto de larga duración es un objeto de una aplicación de servidor que contiene datos estáticos que están activos mientras dura el proceso.

    Los objetos de la generación 2 que sobreviven a una recolección se mantienen en esta generación hasta que en una recolección posterior se determina que no se pueden alcanzar.

    Los objetos del montón de objetos grandes (a veces denominado generación 3) también se recopilan en la generación 2.

Las recolecciones de basura se producen en generaciones específicas según las condiciones lo requieran. La recolección de una generación significa recolectar los objetos de esa generación y de todas las generaciones anteriores. Una recolección de basura de la generación 2 también se conoce como recolección de basura total, porque recupera los objetos de todas las generaciones (es decir, todos los objetos en el montón administrado).

Supervivencia y promociones

Los objetos que no se recolectan en una recolección de basura se conocen como supervivientes y se promueven a la siguiente generación.

  • Los objetos que sobreviven a una recolección de elementos no utilizados de la generación 0 se promueven a la generación 1.
  • Los objetos que sobreviven a una recolección de basura de la generación 1 se promueven a la generación 2.
  • Los objetos que sobreviven a una recolección de basura de la generación 2 permanecen en la generación 2.

Cuando el recolector de basura detecta que el índice de supervivencia es alto en una generación, aumenta el umbral de asignaciones para esa generación. La siguiente colección obtiene un tamaño sustancial de memoria recuperada. El CLR equilibra continuamente dos prioridades: no permitir que el espacio de trabajo de una aplicación adquiera un tamaño excesivo al retrasar la recolección de elementos no utilizados y no permitir que la recolección de elementos no utilizados se ejecute con mucha frecuencia.

Generaciones y segmentos efímeros

Dado que los objetos de las generaciones 0 y 1 son de corta duración, estas generaciones se denominan generaciones efímeras.

Las generaciones efímeras se asignan en el segmento de memoria denominado segmento efímero. Cada nuevo segmento adquirido por el recolector de elementos no utilizados se convierte en el nuevo segmento efímero y contiene los objetos que sobrevivieron a una recolección de elementos no utilizados de la generación 0. El segmento efímero anterior se convierte en el nuevo segmento de la generación 2.

El tamaño del segmento efímero varía según si un sistema es de 32 bits o de 64 bits, y del tipo de recolector de basura que está en ejecución (GC de estación de trabajo o servidor). En la tabla siguiente se muestran los tamaños predeterminados del segmento efímero:

GC de servidor/estación de trabajo 32 bits 64 bits
Estación de trabajo GC 16 MB 256 MB
Servidor de catálogo global 64 MB 4 GB
Servidor GC con > 4 CPU lógicas 32 MB 2 GB
Servidor GC con > 8 CPU lógicas 16 MB 1 GB

El segmento efímero puede incluir objetos de la generación 2. Los objetos de la generación 2 pueden utilizar varios segmentos, tantos como necesite el proceso y la memoria permita.

La cantidad de memoria liberada por una recolección de basura efímera se limita al tamaño del segmento efímero. La cantidad de memoria que se libera es proporcional al espacio que ocupaban los objetos muertos.

Lo que sucede durante la recolección de basura

Una recolección de basura tiene las siguientes fases:

  • Una fase de marcado que busca y crea una lista de todos los objetos activos.

  • Fase de reubicación que actualiza las referencias a los objetos compactados.

  • Una fase de compactación, que reclama el espacio ocupado por los objetos muertos y compacta los objetos supervivientes. En la fase de compactación, se mueven los objetos que han sobrevivido a una recolección de basura hacia el extremo anterior del segmento.

    Debido a que las recolecciones de la generación 2 pueden ocupar varios segmentos, los objetos que se promueven a la generación 2 se pueden mover a un segmento anterior. Los supervivientes de las generaciones 1 y 2 se pueden mover a un segmento diferente, porque se promueven a la generación 2.

    Normalmente, el montón de objetos grandes (LOH) no se compacta, porque al copiar objetos grandes se reduce el rendimiento. Sin embargo, en .NET Core y en .NET Framework 4.5.1 y versiones posteriores, se puede utilizar la propiedad GCSettings.LargeObjectHeapCompactionMode para compactar el montón de objetos grandes a petición. Además, el montón de objetos grandes se compacta automáticamente cuando se establece un límite máximo especificando alguna de las siguientes opciones:

El recolector de basura utiliza la siguiente información para determinar si los objetos están vivos.

  • Raíces de la pila: variables de pila que proporciona el compilador Just-In-Time (JIT) y el rastreador de pila. Las optimizaciones de JIT pueden prolongar o acortar regiones de código en las que se notifican variables de pila al recolector de elementos no utilizados.

  • Identificadores de recolección de basura: identificadores que apuntan a objetos administrados y que se pueden asignar mediante código de usuario o el Common Language Runtime.

  • Datos estáticos: objetos estáticos de dominios de aplicación que podrían hacer referencia a otros objetos. Cada dominio de aplicación realiza el seguimiento de sus objetos estáticos.

Antes de que se inicie una recolección de basura, todos los hilos administrados se suspenden salvo el hilo que activó la recolección de basura.

La siguiente ilustración muestra un subproceso que desencadena una recolección de basura, lo que provoca que los demás subprocesos se suspendan.

Captura de pantalla en la que se muestra cómo un subproceso desencadena una recolección de basura.

Recursos no administrados

En la mayoría de los objetos que crea la aplicación, puede basarse en la recolección de elementos no utilizados para realizar automáticamente las tareas de administración de memoria necesarias. Sin embargo, los recursos no administrados requieren una limpieza explícita. El tipo más habitual de recurso no administrado es un objeto que contiene un recurso del sistema operativo, como un identificador de archivo, identificador de ventana o conexión de red. Aunque el recolector de elementos no utilizados puede realizar el seguimiento del periodo de duración de un objeto administrado que encapsula un recurso no administrado, no tiene un conocimiento específico de cómo limpiar el recurso.

Cuando se define un objeto que encapsula un recurso no administrado, es recomendable proporcionar el código necesario para limpiar ese recurso en un método público Dispose. Si se proporciona un método Dispose, se permite que los usuarios del objeto liberen el recurso de manera explícita cuando hayan terminado de usarlo. Si se usa un objeto que encapsula un recurso no administrado, asegúrese de llamar a Dispose cuando sea necesario.

También debe proporcionar una forma de liberar los recursos no administrados en el caso de que un consumidor de su tipo olvide llamar a Dispose. Puede usar un controlador seguro para encapsular el recurso no administrado o invalidar el método Object.Finalize().

Para obtener más información sobre la limpieza de recursos no administrados, vea Limpieza de recursos no administrados.

Vea también