recolector basura c# .net .net-3.5 garbage-collection

Monitoreo de recolector de basura en C#



destructor c# (1)

Tengo una aplicación WPF que está experimentando muchos problemas de rendimiento. Lo peor de ellos es que a veces la aplicación simplemente se congela durante unos segundos antes de volver a ejecutarse.

Actualmente estoy depurando la aplicación para ver con qué podría estar relacionado este bloqueo, y creo que una de las cosas que puede estar causando esto es el recolector de basura. Como mi aplicación se ejecuta en un entorno muy limitado, creo que el recolector de basura puede estar utilizando todos los recursos de la máquina cuando se ejecuta y no deja ninguno en nuestra aplicación.

Para verificar estas hipótesis, encontré estos artículos: Notificaciones de recolección de basura y Notificaciones de recolección de basura en .NET 4.0 , que explican cómo se puede notificar a mi aplicación cuando el recolector de basura comenzará a ejecutarse y cuando finalice.

Entonces, en base a esos artículos, creé la clase a continuación para recibir las notificaciones:

public sealed class GCMonitor { private static volatile GCMonitor instance; private static object syncRoot = new object(); private Thread gcMonitorThread; private ThreadStart gcMonitorThreadStart; private bool isRunning; public static GCMonitor GetInstance() { if (instance == null) { lock (syncRoot) { instance = new GCMonitor(); } } return instance; } private GCMonitor() { isRunning = false; gcMonitorThreadStart = new ThreadStart(DoGCMonitoring); gcMonitorThread = new Thread(gcMonitorThreadStart); } public void StartGCMonitoring() { if (!isRunning) { gcMonitorThread.Start(); isRunning = true; AllocationTest(); } } private void DoGCMonitoring() { long beforeGC = 0; long afterGC = 0; try { while (true) { // Check for a notification of an approaching collection. GCNotificationStatus s = GC.WaitForFullGCApproach(10000); if (s == GCNotificationStatus.Succeeded) { //Call event beforeGC = GC.GetTotalMemory(false); LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC is about to begin. Memory before GC: %d", beforeGC); GC.Collect(); } else if (s == GCNotificationStatus.Canceled) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was cancelled"); } else if (s == GCNotificationStatus.Timeout) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was timeout"); } else if (s == GCNotificationStatus.NotApplicable) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was not applicable"); } else if (s == GCNotificationStatus.Failed) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event failed"); } // Check for a notification of a completed collection. s = GC.WaitForFullGCComplete(10000); if (s == GCNotificationStatus.Succeeded) { //Call event afterGC = GC.GetTotalMemory(false); LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC has ended. Memory after GC: %d", afterGC); long diff = beforeGC - afterGC; if (diff > 0) { LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "Collected memory: %d", diff); } } else if (s == GCNotificationStatus.Canceled) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was cancelled"); } else if (s == GCNotificationStatus.Timeout) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was timeout"); } else if (s == GCNotificationStatus.NotApplicable) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was not applicable"); } else if (s == GCNotificationStatus.Failed) { LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event failed"); } Thread.Sleep(1500); } } catch (Exception e) { LogHelper.Log.Error(" ******************** Garbage Collector Error ************************ "); LogHelper.LogAllErrorExceptions(e); LogHelper.Log.Error(" ------------------- Garbage Collector Error --------------------- "); } } private void AllocationTest() { // Start a thread using WaitForFullGCProc. Thread stress = new Thread(() => { while (true) { List<char[]> lst = new List<char[]>(); try { for (int i = 0; i <= 30; i++) { char[] bbb = new char[900000]; // creates a block of 1000 characters lst.Add(bbb); // Adding to list ensures that the object doesnt gets out of scope } Thread.Sleep(1000); } catch (Exception ex) { LogHelper.Log.Error(" ******************** Garbage Collector Error ************************ "); LogHelper.LogAllErrorExceptions(e); LogHelper.Log.Error(" ------------------- Garbage Collector Error --------------------- "); } } }); stress.Start(); } }

Y he agregado la opción gcConcurrent a mi archivo app.config (a continuación):

<?xml version="1.0"?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net-net-2.0"/> </configSections> <runtime> <gcConcurrent enabled="false" /> </runtime> <log4net> <appender name="Root.ALL" type="log4net.Appender.RollingFileAppender"> <param name="File" value="../Logs/Root.All.log"/> <param name="AppendToFile" value="true"/> <param name="MaxSizeRollBackups" value="10"/> <param name="MaximumFileSize" value="8388608"/> <param name="RollingStyle" value="Size"/> <param name="StaticLogFileName" value="true"/> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%date [%thread] %-5level - %message%newline"/> </layout> </appender> <root> <level value="ALL"/> <appender-ref ref="Root.ALL"/> </root> </log4net> <appSettings> <add key="setting1" value="1"/> <add key="setting2" value="2"/> </appSettings> <startup> <supportedRuntime version="v2.0.50727"/> </startup> </configuration>

Sin embargo, cada vez que se ejecuta la aplicación, parece que no se envía ninguna notificación de que se ejecutará el recolector de basura. He puesto puntos de interrupción en la supervisión de DoGC y parece que las condiciones (s == GCNotificationStatus.Succeeded) y (s == GCNotificationStatus.Succeeded) nunca se cumplen, por lo tanto, el contenido de esas declaraciones ifs nunca se ejecutan.

¿Qué estoy haciendo mal?

Nota: estoy utilizando C # con WPF y .NET Framework 3.5.

ACTUALIZACIÓN 1

Actualicé mi prueba GCMonitor con el método AllocationTest. Este método es sólo para fines de prueba. Solo quería asegurarme de que se estaba asignando suficiente memoria para forzar la ejecución del recolector de basura.

ACTUALIZACIÓN 2

Se actualizó el método DoGCMonitoring, con nuevas comprobaciones en la devolución de los métodos WaitForFullGCApproach y WaitForFullGCComplete. Por lo que he visto hasta ahora, mi aplicación va directamente a la condición (s == GCNotificationStatus.NotApplicable). Así que creo que tengo una mala configuración en algún lugar que me impide obtener los resultados deseados.

La documentación para la enumeración GCNotificationStatus se puede encontrar here .


No veo GC.RegisterForFullGCNotification(int,int) en ninguna parte de su código. Parece que estás usando los WaitForFullGC[xxx] , pero nunca te estás registrando para WaitForFullGC[xxx] la notificación. Probablemente es por eso que obtienes el estado NotApplicable.

Sin embargo, dudo que GC sea tu problema, mientras sea posible, supongo que sería bueno saber acerca de todos los modos de GC que existen y las mejores maneras de determinar qué está sucediendo. Hay dos modos de recolección de basura en .NET: el servidor y la estación de trabajo. Ambos recopilan la misma memoria no utilizada, sin embargo, la forma en que se hace es ligeramente diferente.

  • Versión del servidor : este modo le indica al GC que está utilizando una aplicación del lado del servidor e intenta optimizar las colecciones para estos escenarios. Se dividirá el montón en varias secciones, 1 por CPU. Cuando se inicie el GC, se ejecutará un subproceso en cada CPU en paralelo. Realmente quieres múltiples CPU para que esto funcione bien. Si bien la versión del servidor utiliza varios subprocesos para el GC, no es lo mismo que el modo GC de la estación de trabajo concurrente que se detalla a continuación. Cada hilo actúa como la versión no concurrente.

  • Versión de estación de trabajo : este modo le dice a GC que está usando una aplicación del lado del cliente. Parece que tiene más recursos limitados que la versión del servidor, por lo que solo hay un subproceso GC. Sin embargo, hay dos configuraciones de la versión de la estación de trabajo: concurrente y no concurrente.

    • Concurrente : esta es la versión activada de forma predeterminada cada vez que se utiliza la estación de trabajo GC (este sería el caso de su aplicación WPF). El GC siempre se ejecuta en un subproceso independiente que siempre marca los objetos para la colección cuando la aplicación se está ejecutando. Además, elige si compactar o no la memoria en ciertas generaciones y toma esa decisión en función del rendimiento. Todavía debe congelar todos los subprocesos para ejecutar una colección si se realiza la compactación, pero casi nunca verá una aplicación que no responde cuando se usa este modo. Esto crea una mejor experiencia interactiva para usos y es mejor para aplicaciones de consola o GUI.
    • No concurrente : esta es una versión que puede configurar su aplicación para usar, si lo desea. En este modo, el subproceso del GC duerme hasta que se inicia un GC, luego va y marca todos los árboles de objetos que son basura, libera la memoria y lo compacta, todo mientras todos los demás subprocesos están suspendidos. Esto puede hacer que la aplicación a veces deje de responder por un corto período de tiempo.

No puede registrarse para recibir notificaciones en el recopilador simultáneo, ya que se realiza en segundo plano. Es posible que su aplicación no esté utilizando el recopilador concurrente (me doy cuenta de que tiene el gcConcurrent deshabilitado en el app.config , pero parece que eso es solo para pruebas). Si ese es el caso, ciertamente puede ver cómo se congela su aplicación si hay colecciones pesadas. Por eso crearon el coleccionista concurrente. El tipo de modo GC se puede configurar parcialmente en el código y se puede configurar completamente en las configuraciones de la aplicación y la configuración de la máquina.

¿Qué podemos hacer para averiguar exactamente qué está utilizando nuestra aplicación? En tiempo de ejecución, puede consultar la clase estática GCSettings (en System.Runtime ). GCSettings.IsServerGC le dirá si está ejecutando la estación de trabajo en las versiones del servidor y GCSettings.LatencyMode puede decir si está usando una versión concurrente, no concurrente o especial que debe establecer en el código que no es realmente aplicable aquí. Creo que sería un buen lugar para comenzar y podría explicar por qué funciona bien en su máquina, pero no en producción.

En los archivos de configuración, <gcConcurrent enabled="true|false"/> o <gcServer enabled="true|false"/> controlan los modos del recolector de basura. Tenga en cuenta que esto puede estar en su archivo app.config (ubicado al lado del ensamblaje en ejecución) o en el archivo machine.config, que se encuentra en %windir%/Microsoft.NET/Framework/[version]/CONFIG/

También puede usar el Monitor de rendimiento de Windows de forma remota para acceder a los contadores de rendimiento de la máquina de producción para la recolección de basura .NET y ver esas estadísticas. Puede hacer lo mismo con el seguimiento de eventos para Windows (ETW) todo de forma remota. Para el monitor de rendimiento, querría el objeto de .NET CLR Memory y seleccionar su aplicación en el cuadro de lista de instancias.