c# - programacion - que es c sharp
La recolección de basura en C#no se lleva a cabo. ¿Por qué? (5)
El recolector de basura .NET es una bestia de software altamente optimizada y complicada. Está optimizado para hacer que su programa se ejecute lo más rápido posible y no usar demasiada memoria al hacerlo.
Debido a que el proceso de liberar memoria lleva algo de tiempo, el recolector de basura a menudo espera ejecutarlo hasta que el programa utilice mucha memoria. Luego hace todo el trabajo a la vez, lo que resulta en un pequeño retraso de su programa después de un tiempo relativamente largo (en lugar de retrasos más pequeños antes, lo que ralentizaría su programa).
Todo esto significa que el tiempo que el recolector de basura se ejecuta no es predecible .
Puede llamar a su prueba varias veces (con algo de reposo () en el ciclo) y ver cómo el uso de la memoria se va acumulando lentamente. Cuando su programa comience a consumir una parte significativa de la memoria física disponible, su uso de memoria caerá repentinamente a casi cero.
Hay un par de funciones (como GC.Collect()
) que fuerzan varios niveles de recolección de basura, pero se recomienda no utilizarlas a menos que sepa lo que está haciendo, ya que esto hace que el software sea más lento y detiene la basura. colector en hacer su trabajo de una manera óptima.
Intenté un experimento simple para verificar la funcionalidad del recolector de basura. Referencia 3.9 Gestión automática de memoria (MSDN) sobre la administración automática de memoria en .NET . Para mí, sonaba como un puntero compartido equivalente en C ++. Si el contador de referencia de un objeto se vuelve cero, será desasignado por el recolector de basura.
Así que intenté crear una función dentro de mi formulario principal. La función se llamó dentro de la función de evento Mostrado de mi formulario principal que se ejecuta después del constructor. Aquí está el código experimental.
public void experiment()
{
int[] a = new int[100000];
int[] b = new int[100000];
int[] c = new int[100000];
int[] d = new int[100000];
a = null;
b = null;
c = null;
d = null;
}
Y aquí están los resultados:
Antes de la asignación de memoria
Después de la asignación de memoria
Antes de abandonar el alcance de la función
Después de salir del alcance de la función
¿Por qué el recolector de basura no desasoció la memoria asignada por las matrices a, b, c, d incluso después de haberse establecido en nulo?
Incluso si se desasignó la memoria internamente, no está obligado a devolverla al sistema operativo. Supondrá que se solicitará más memoria en el futuro y reciclará las páginas. El número del sistema operativo no sabe nada de cómo el programa ha elegido usar la memoria que ha reclamado.
Si realmente desea reclamar y liberar memoria explícitamente, deberá llamar a VirtualAlloc () a través de Pinvoke código inseguro.
La recolección de basura es costosa. Solo quieres que se ejecute lo menos posible. Idealmente nunca Por lo tanto, el sistema intentará retrasar la recolección de basura tanto como pueda, básicamente hasta que se quede sin memoria.
Asignar memoria es costoso. Una vez que el tiempo de ejecución ha asignado algo de memoria, normalmente no la liberará nuevamente, incluso si actualmente no la necesita, porque si necesita tanta memoria durante un tiempo del tiempo de ejecución del programa, es probable que necesite liberarla. cantidades similares de memoria en algún momento en el futuro y quiere evitar tener que asignar memoria de nuevo.
Entonces, incluso si la recolección de basura ocurrió durante su prueba, no la vería en el Administrador de tareas o en el Explorador de procesos, porque de todos modos la CLR no la liberaría.
Lo que está describiendo se llama colector de basura que cuenta la referencia . Sin embargo, todas las implementaciones actuales de CLI VES usan un GC de rastreo . Los GC de seguimiento no cuentan las referencias; ellos los rastrean, solo cuando están corriendo . Un GC de rastreo no notará si un objeto todavía es alcanzable o no hasta que rastree el gráfico del objeto, y solo rastreará el gráfico del objeto cuando necesite ejecutar una colección, es decir, cuando se quede sin memoria.
Parte de la información ya está incluida en el artículo al que se vincula. Hay varias indicaciones de que el comportamiento que observa es correcto:
... el recolector de basura puede (aunque no es obligatorio) tratar el objeto como si ya no estuviera en uso.
... en algún momento posterior no especificado ...
GC.Collect ()
Una cosa importante, al menos para la versión anterior (no simultánea) del recolector de basura es que el recolector de basura se ejecuta en un hilo diferente. Puede verificar eso en el depurador:
0:003> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 1b08 0058f218 a020 Enabled 025553ac:02555fe8 0058b868 1 MTA
2 2 1e9c 005a78c8 b220 Enabled 00000000:00000000 0058b868 0 MTA (Finalizer)
El subproceso Finalizer realiza la recolección de elementos no utilizados. Todos los demás subprocesos se suspenden durante la operación, de modo que ningún subproceso puede modificar objetos durante el tiempo de reorganización.
Pero, ¿por qué es eso tan importante?
Explica por qué la recolección de basura no se aplica inmediatamente, ni en su escenario ni si llama a GC.Collect()
para hacer la recolección de basura. Para que se ejecute el recolector de basura, también necesita un cambio de hilo. Por lo tanto, el código mínimo necesario para una recolección de basura no concurrente es
GC.Collect();
Thread.Sleep(0);
Si le preocupa el manejo de la memoria, asegúrese también de ver la impresionante respuesta sobre IDisposable .
Memoria libre
Además, nadie ha explicado aún, que mirar el consumo de memoria con el Administrador de tareas no es confiable.
.NET actúa directamente en la memoria virtual, es decir, utiliza el administrador de memoria virtual. No usa el montón, es decir, el administrador de montón. En su lugar, usa su propia administración de memoria, llamada almacenamiento administrado.
.NET obtiene la memoria de Windows (el kernel). Supongamos que obtiene una pieza nueva de memoria de Windows, que no tiene objetos .NET adentro. Desde el punto de vista de Windows, la memoria se ha ido (dada a .NET). Sin embargo, desde el punto de vista de .NET, es gratuito y puede ser utilizado por objetos.
De nuevo, puedes observar eso en el depurador:
0:003> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 60 71cb9000 ( 1.778 Gb) 88.91%
<unknown> 84 986f000 ( 152.434 Mb) 67.09% 7.44%
Image 189 2970000 ( 41.438 Mb) 18.24% 2.02%
...
Lo que se informa como <unknown>
es la memoria virtual desde el punto de vista de Windows. En este caso, se usan 150 MB.
0:003>!dumpheap -stat
...
00672208 32 8572000 Free
...
De modo que puede ver que 8.5 MB están libres del punto de vista de .NET, pero no se han devuelto a Windows (todavía) y se seguirán informando como utilizados allí.
Conjunto de trabajo de medición
Si no ha modificado la configuración predeterminada de la columna del Administrador de tareas, es aún peor, porque mostrará el Conjunto de trabajo, que es la memoria en la RAM solamente. Sin embargo, es posible que parte de la memoria se haya intercambiado en el disco, por lo que el Administrador de tareas no puede informarlo.
CLR no ejecuta el recolector de elementos no utilizados para cada versión de memoria, ya que consume recursos del sistema. Por lo tanto, el recolector de basura se llama a intervalos regulares en función del tamaño de la memoria en crecimiento. Borraría todas las pérdidas de memoria no retenidas.
También el recolector de elementos no utilizados se puede llamar de forma explícita utilizando el método GC.Collect (), pero no es aconsejable utilizarlo explícitamente.