¿Cuándo es aceptable llamar a GC.Collect?

166

El consejo general es que no debe llamar GC.Collectdesde su código, pero ¿cuáles son las excepciones a esta regla?

Solo puedo pensar en algunos casos muy específicos en los que puede tener sentido forzar una recolección de basura.

Un ejemplo que me viene a la mente es un servicio, que se despierta a intervalos, realiza alguna tarea y luego duerme durante mucho tiempo. En este caso, puede ser una buena idea forzar una recopilación para evitar que el proceso que pronto estará inactivo retenga más memoria de la necesaria.

¿Hay otros casos en los que es aceptable llamar GC.Collect?

Brian Rasmussen
fuente

Respuestas:

153

Si tiene buenas razones para creer que un conjunto significativo de objetos, particularmente aquellos que sospecha que son de las generaciones 1 y 2, ahora son elegibles para la recolección de basura, y ese sería un momento apropiado para recolectar en términos del pequeño impacto en el rendimiento .

Un buen ejemplo de esto es si acaba de cerrar un formulario grande. Usted sabe que todos los controles de la interfaz de usuario ahora se pueden recolectar basura, y una pausa muy corta a medida que se cierra el formulario probablemente no sea notoria para el usuario.

ACTUALIZACIÓN 2.7.2018

A partir de .NET 4.5, hay GCLatencyMode.LowLatencyy GCLatencyMode.SustainedLowLatency. Al entrar y salir de cualquiera de estos modos, se recomienda que fuerce un GC completo conGC.Collect(2, GCCollectionMode.Forced) .

A partir de .NET 4.6, existe el GC.TryStartNoGCRegionmétodo (utilizado para establecer el valor de solo lectura GCLatencyMode.NoGCRegion). Esto puede, en sí mismo, realizar una recolección de basura de bloqueo completo en un intento de liberar suficiente memoria, pero dado que no permitimos GC durante un período, diría que también es una buena idea realizar GC completo antes y después.

Fuente: ingeniero de Microsoft Ben Watson: escritura de código .NET de alto rendimiento , 2ª ed. 2018.

Ver:

Jon Skeet
fuente
8
Según el código fuente de MS que llama a GC.Collect (2) cada 850 ms está bien. No creo Luego solo vea PresentationCore.dll, MS.Internal.MemoryPressure.ProcessAdd (). Actualmente tengo una aplicación de procesamiento de imágenes (imágenes pequeñas, nada con presión de memoria real) donde llamar a GC.Collect (2) lleva más de 850 ms, por lo que toda la aplicación se congela por esto (la aplicación pasa el 99.7% del tiempo en GC).
springy76
36
@ springy76: El hecho de que Microsoft lo haga en un solo lugar no significa que sea considerado algo bueno por aquellos que dan consejos de Microsoft ...
Jon Skeet
44
No me gusta ese ejemplo. ¿Cuál es el resultado al hacerlo después de cerrar el formulario? Un buen ejemplo que veo es para después de cargar el nivel del juego en XBox o WindowsPhone. En esas plataformas, GC se ejecuta después de asignar 1 MB o algo así. Por lo tanto, es bueno asignar tanto como sea posible durante la carga del nivel (mientras se muestra una pantalla de presentación) y luego hacer GC.Recoge para tratar de evitar las colecciones durante el juego.
Piotr Perak
8
@Peri: El punto de hacerlo después de cerrar el formulario es que acabas de hacer que un grupo de objetos (controles, los datos que estabas mostrando) sean elegibles para la recolección de basura, así que al llamar GC.Collect, básicamente le estás diciendo al recolector de basura que sabes mejor que para un cambio. ¿Por qué no te gusta el ejemplo?
Jon Skeet
66
@SHCJ: GC.Collect()solicitará que el GC realice una recopilación completa . Si sabe que acaba de hacer que muchos objetos de larga vida anterior sean elegibles para la recolección de basura y cree que es menos probable que el usuario note una pequeña pausa ahora que más tarde, parece completamente razonable pensar que ahora es mejor hora de solicitar una colección que dejar que suceda más tarde.
Jon Skeet
50

Solo lo uso GC.Collectal escribir plataformas de prueba de rendimiento / perfilador de crudo; es decir, tengo dos (o más) bloques de código para probar, algo como:

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestA(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestB(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
...

De manera que TestA()y TestB()ejecutar la mayor estado similar como sea posible - es decir, TestB()no consigue martillado sólo porque TestAlo dejó muy cerca del punto de inflexión.

Un ejemplo clásico sería un simple exe de consola (un Mainmétodo lo suficientemente ordenado para ser publicado aquí, por ejemplo), que muestra la diferencia entre la concatenación de cadenas en bucle y StringBuilder.

Si necesito algo preciso, entonces serían dos pruebas completamente independientes, pero a menudo esto es suficiente si solo queremos minimizar (o normalizar) el GC durante las pruebas para tener una idea aproximada del comportamiento.

Durante el código de producción? Todavía tengo que usarlo ;-p

Marc Gravell
fuente
1
Y probablemente agregue "WaitForPendingFinalizers" (o lo que sea) también en este caso ;-p
Marc Gravell
29

La mejor práctica es no forzar una recolección de basura en la mayoría de los casos. (Todos los sistemas en los que he trabajado que forzaron la recolección de basura, tuvieron problemas que subrayaron que si se resuelven habrían eliminado la necesidad de forzar la recolección de basura, y aceleraron mucho el sistema).

Hay algunos casos en los que sabe más sobre el uso de la memoria que el recolector de basura. Es poco probable que esto sea cierto en una aplicación multiusuario o en un servicio que responde a más de una solicitud a la vez.

Sin embargo, en algunos tipos de procesamiento por lotes, usted sabe más que el GC. Por ejemplo, considere una aplicación que.

  • Se le da una lista de nombres de archivo en la línea de comando
  • Procesa un solo archivo y luego escribe el resultado en un archivo de resultados.
  • Mientras procesa el archivo, crea muchos objetos interconectados que no se pueden recopilar hasta que el procesamiento del archivo se haya completado (por ejemplo, un árbol de análisis)
  • No mantiene el estado de coincidencia entre los archivos que ha procesado .

Es posible que pueda hacer un caso (después de una cuidadosa) prueba de que debe forzar una recolección de basura completa después de haber procesado cada archivo.

Otro caso es un servicio que se activa cada pocos minutos para procesar algunos elementos y no mantiene ningún estado mientras está dormido . Entonces, forzar una colección completa justo antes de irse a dormir puede valer la pena.

La única vez que consideraría forzar una colección es cuando sé que recientemente se ha creado una gran cantidad de objetos y actualmente se hace referencia a muy pocos objetos.

Preferiría tener una API de recolección de basura cuando pudiera darle pistas sobre este tipo de cosas sin tener que forzar un GC por mí mismo.

Ver también " Tidbits de rendimiento de Rico Mariani "

Ian Ringrose
fuente
11

En sistemas grandes 24/7 o 24/6, sistemas que reaccionan a mensajes, solicitudes RPC o que sondean una base de datos o procesan continuamente, es útil tener una forma de identificar pérdidas de memoria. Para esto, tiendo a agregar un mecanismo a la aplicación para suspender temporalmente cualquier procesamiento y luego realizar la recolección de basura completa. Esto pone al sistema en un estado de reposo donde la memoria restante es legítimamente memoria de larga duración (cachés, configuración, etc.) o se 'filtra' (objetos que no se espera o se desea que se arraiguen, pero en realidad lo están).

Tener este mecanismo hace que sea mucho más fácil perfilar el uso de la memoria, ya que los informes no se verán nublados por el ruido del procesamiento activo.

Para asegurarse de obtener toda la basura, debe realizar dos recolecciones:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Como la primera colección causará que se finalicen los objetos con finalizadores (pero no la recolección de basura en realidad estos objetos). El segundo GC recogerá basura estos objetos finalizados.

Paul Ruane
fuente
He visto la colección de dos pasadas en un par de lugares ahora, pero después de leer el pasaje en la documentación de MSDN para GC.WaitForPendingFinalizers que dice: "Espere a que se completen todos los finalizadores antes de continuar. Sin esta llamada a GC.WaitForPendingFinalizers, el ciclo de trabajo a continuación podría ejecutarse al mismo tiempo que los finalizadores. Con esta llamada, el ciclo de trabajo se ejecuta solo después de que se hayan llamado todos los finalizadores ". Solo soy un poco paranoico. ¿Conoces una fuente definitiva para hacer dos pases?
jerhewet
1
@jerhewet: La clave para comprender por qué se requieren dos colecciones es comprender qué sucede con los objetos con finalizadores. Desafortunadamente, no tengo exactamente lo que está pidiendo, pero lea este artículo y esta pregunta sobre SO .
Paul Ruane
10

Puede llamar a GC.Collect () cuando sepa algo sobre la naturaleza de la aplicación que el recolector de basura no conoce. Es tentador pensar que, como el autor, esto es muy probable. Sin embargo, la verdad es que el GC equivale a un sistema experto bastante bien escrito y probado, y es raro que sepa algo sobre las rutas de código de bajo nivel que no lo hace.

El mejor ejemplo que puedo pensar sobre dónde podría tener información adicional es una aplicación que alterna entre períodos inactivos y períodos muy ocupados. Desea el mejor rendimiento posible para los períodos de mayor actividad y, por lo tanto, desea utilizar el tiempo de inactividad para hacer una limpieza.

Sin embargo, la mayoría de las veces el GC es lo suficientemente inteligente como para hacer esto de todos modos.

Joel Coehoorn
fuente
8

Como una solución de fragmentación de memoria. Me estaba quedando sin excepciones de memoria mientras escribía muchos datos en una secuencia de memoria (lectura de una secuencia de red). Los datos se escribieron en fragmentos de 8K. Después de alcanzar los 128M hubo una excepción a pesar de que había mucha memoria disponible (pero estaba fragmentada). Llamar a GC.Collect () resolvió el problema. Pude manejar más de 1G después de la corrección.

Carácter distintivo
fuente
7

Echa un vistazo a este artículo de Rico Mariani. Da dos reglas cuando llamar a GC.Collect (la regla 1 es: "No"):

Cuándo llamar a GC.Collect ()

M4N
fuente
3
Ya estuve allí. No estoy tratando de encontrar excusas para hacer algo que no deberías hacer, pero me gustaría saber si hay casos específicos en los que sería aceptable.
Brian Rasmussen
5

Una instancia en la que es casi necesario llamar a GC.Collect () es cuando se automatiza Microsoft Office a través de Interop. A los objetos COM para Office no les gusta lanzarse automáticamente y pueden dar lugar a que las instancias del producto de Office consuman grandes cantidades de memoria. No estoy seguro de si esto es un problema o por diseño. Hay muchas publicaciones sobre este tema en Internet, por lo que no entraré en demasiados detalles.

Cuando se programa utilizando Interop, cada objeto COM debe liberarse manualmente, generalmente mediante el uso de Marshal.ReleseComObject (). Además, llamar a Recolección de basura manualmente puede ayudar a "limpiar" un poco. Llamar al siguiente código cuando haya terminado con los objetos de Interop parece ayudar bastante:

GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()

En mi experiencia personal, usar una combinación de ReleaseComObject y llamar manualmente a la recolección de basura reduce en gran medida el uso de memoria de los productos de Office, específicamente Excel.

garthhh
fuente
Sí, me encontré con eso con .net accediendo a Excel que también funciona a través de objetos com. Es importante tener en cuenta que esto no funcionará bien en el modo DEPURACIÓN porque las operaciones de GC están limitadas allí. Funcionará como solo en el modo RELEASE. enlace relevante: stackoverflow.com/questions/17130382/…
Blechdose
5

Estaba haciendo algunas pruebas de rendimiento en la matriz y la lista:

private static int count = 100000000;
private static List<int> GetSomeNumbers_List_int()
{
    var lstNumbers = new List<int>();
    for(var i = 1; i <= count; i++)
    {
        lstNumbers.Add(i);
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Array()
{
    var lstNumbers = new int[count];
    for (var i = 1; i <= count; i++)
    {
        lstNumbers[i-1] = i + 1;
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Enumerable_Range()
{
    return  Enumerable.Range(1, count).ToArray();
}

static void performance_100_Million()
{
    var sw = new Stopwatch();

    sw.Start();
    var numbers1 = GetSomeNumbers_List_int();
    sw.Stop();
    //numbers1 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"List<int>\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
    var numbers2 = GetSomeNumbers_Array();
    sw.Stop();
    //numbers2 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"int[]\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
//getting System.OutOfMemoryException in GetSomeNumbers_Enumerable_Range method
    var numbers3 = GetSomeNumbers_Enumerable_Range();
    sw.Stop();
    //numbers3 = null;
    //GC.Collect();

    Console.WriteLine(String.Format("\"int[]\" Enumerable.Range took {0} milliseconds", sw.ElapsedMilliseconds));
}

y obtuve el OutOfMemoryExceptionmétodo GetSomeNumbers_Enumerable_Range, la única solución es desasignar la memoria mediante:

numbers = null;
GC.Collect();
Daniel B
fuente
¿Por qué votar abajo? mi respuesta es un ejemplo que demuestra cuándo llamar a GC. ¿Tiene alguna sugerencia mejor? Le invitamos a presentar.
Daniel B
4

En su ejemplo, creo que llamar a GC.Collect no es el problema, sino que hay un problema de diseño.

Si va a despertarse a intervalos (tiempos establecidos), entonces su programa debe estar diseñado para una sola ejecución (realizar la tarea una vez) y luego finalizar. Luego, configura el programa como una tarea programada para ejecutarse en los intervalos programados.

De esta manera, no tiene que preocuparse por llamar a GC.Collect (lo cual rara vez debería hacer).

Dicho esto, Rico Mariani tiene una gran publicación de blog sobre este tema, que se puede encontrar aquí:

http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx

casperOne
fuente
3

Un lugar útil para llamar a GC.Collect () es en una prueba unitaria cuando desea verificar que no está creando una pérdida de memoria (por ejemplo, si está haciendo algo con WeakReferences o ConditionalWeakTable, código generado dinámicamente, etc.).

Por ejemplo, tengo algunas pruebas como:

WeakReference w = CodeThatShouldNotMemoryLeak();
Assert.IsTrue(w.IsAlive);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsFalse(w.IsAlive);

Se podría argumentar que usar WeakReferences es un problema en sí mismo, pero parece que si está creando un sistema que se basa en ese comportamiento, entonces llamar a GC.Collect () es una buena manera de verificar dicho código.

ChaseMedallion
fuente
2
using(var stream = new MemoryStream())
{
   bitmap.Save(stream, ImageFormat.Png);
   techObject.Last().Image = Image.FromStream(stream);
   bitmap.Dispose();

   // Without this code, I had an OutOfMemory exception.
   GC.Collect();
   GC.WaitForPendingFinalizers();
   //
}
Denis
fuente
2

Hay algunas situaciones en las que es mejor prevenir que curar.

Aquí hay una situación.

Es posible crear una DLL no administrada en C # usando reescrituras de IL (porque hay situaciones en las que esto es necesario).

Ahora supongamos, por ejemplo, que la DLL crea una matriz de bytes en el nivel de clase, porque muchas de las funciones exportadas necesitan acceso a ellas. ¿Qué sucede cuando se descarga la DLL? ¿Se llama automáticamente al recolector de basura en ese punto? No lo sé, pero al ser una DLL no administrada , es muy posible que no se llame al GC. Y sería un gran problema si no se llamara. Cuando se descarga la DLL, también sería el recolector de basura, entonces, ¿quién será responsable de recolectar cualquier posible basura y cómo lo harían? Es mejor emplear el recolector de basura de C #. Tiene una función de limpieza (disponible para el cliente DLL) donde las variables de nivel de clase se configuran como nulas y se llama al recolector de basura.

Más vale prevenir que lamentar.

Carl Looper
fuente
2

Todavía estoy bastante inseguro sobre esto. Estoy trabajando desde hace 7 años en un servidor de aplicaciones. Nuestras instalaciones más grandes utilizan 24 GB de RAM. Es altamente multiproceso, y TODAS las llamadas para GC.Collect () se encontraron con problemas de rendimiento realmente terribles.

Muchos componentes de terceros usaron GC.Collect () cuando pensaron que era inteligente hacer esto ahora. Entonces, un simple grupo de informes de Excel bloqueó el servidor de aplicaciones para todos los hilos varias veces por minuto.

Tuvimos que refactorizar todos los componentes de terceros para eliminar las llamadas GC.Collect (), y todo funcionó bien después de hacer esto.

Pero también estoy ejecutando servidores en Win32, y aquí comencé a usar GC.Collect () después de obtener una excepción OutOfMemoryException.

Pero también estoy bastante inseguro sobre esto, porque a menudo me di cuenta, cuando obtengo un OOM en 32 Bit, y vuelvo a intentar ejecutar la misma Operación nuevamente, sin llamar a GC.Collect (), simplemente funcionó bien.

Una cosa que me pregunto es la Excepción OOM en sí ... Si hubiera escrito .Net Framework, y no puedo asignar un bloque de memoria, usaría GC.Collect (), defrag memory (??), intente nuevamente , y si todavía no puedo encontrar un bloque de memoria libre, entonces lanzaría la OOM-Exception.

O al menos haga que este comportamiento sea una opción configurable, debido a los inconvenientes del problema de rendimiento con GC.Collect.

Ahora tengo un montón de código como este en mi aplicación para "resolver" el problema:

public static TResult ExecuteOOMAware<T1, T2, TResult>(Func<T1,T2 ,TResult> func, T1 a1, T2 a2)
{

    int oomCounter = 0;
    int maxOOMRetries = 10;
    do
    {
        try
        {
            return func(a1, a2);
        }
        catch (OutOfMemoryException)
        {
            oomCounter++;
            if (maxOOMRetries > 10)
            {
                throw;
            }
            else
            {
                Log.Info("OutOfMemory-Exception caught, Trying to fix. Counter: " + oomCounter.ToString());
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(oomCounter * 10));
                GC.Collect();
            }
        }
    } while (oomCounter < maxOOMRetries);

    // never gets hitted.
    return default(TResult);
}

(Tenga en cuenta que el comportamiento Thread.Sleep () es un comportamiento realmente específico de la aplicación, porque estamos ejecutando un servicio de almacenamiento en caché ORM, y el servicio tarda un tiempo en liberar todos los objetos en caché, si la RAM excede algunos valores predefinidos. unos segundos la primera vez, y ha aumentado el tiempo de espera cada vez que aparece OOM).

Thomas Haller
fuente
Un componente no debería llamar GC.Collect. Dado que tiene un efecto amplio en la aplicación, solo la aplicación debería hacerlo (si es que lo hace).
CodesInChaos
If i would have written the .Net Framework, and i can't alloc a memory block, i would use GC.Collect(),- Creo que ya están haciendo esto - He visto indicios de que uno de los desencadenantes internos del GC son ciertas fallas en la asignación de memoria.
G. Stoynev
2

Debe intentar evitar usar GC.Collect () ya que es muy costoso. Aquí hay un ejemplo:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet.Remove(RecordSet[0]);

        }
        GC.Collect(); // AVOID
    }

RESULTADO DE LA PRUEBA: USO DE LA CPU 12%

Cuando cambias a esto:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet[0].Dispose(); //  Bitmap destroyed!
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet[0].Dispose(); //  Bitmap destroyed!
            RecordSet.Remove(RecordSet[0]);

        }
        //GC.Collect();
    }

RESULTADO DE LA PRUEBA: USO DE LA CPU 2-3%

mtekeli
fuente
1

Otra razón es cuando tiene un SerialPort abierto en un puerto USB COM, y luego el dispositivo USB se desconecta. Debido a que se abrió SerialPort, el recurso contiene una referencia al puerto previamente conectado en el registro del sistema. El registro del sistema entonces contendrá datos obsoletos , por lo que la lista de puertos disponibles será incorrecta. Por lo tanto, el puerto debe estar cerrado.

Llamar a SerialPort.Close () en el puerto llama a Dispose () en el objeto, pero permanece en la memoria hasta que la recolección de basura realmente se ejecute, lo que hace que el registro permanezca obsoleto hasta que el recolector de basura decida liberar el recurso.

Desde https://stackoverflow.com/a/58810699/8685342 :

try
{
    if (port != null)
        port.Close(); //this will throw an exception if the port was unplugged
}
catch (Exception ex) //of type 'System.IO.IOException'
{
    System.GC.Collect();
    System.GC.WaitForPendingFinalizers();
}

port = null;
Jay Tennant
fuente
0

Esto no es tan relevante para la pregunta, pero para las transformaciones XSLT en .NET (XSLCompiledTranform), es posible que no tenga otra opción. Otro candidato es el control MSHTML.

Chris S
fuente
0

Una buena razón para llamar a GC es en computadoras ARM pequeñas con poca memoria, como Raspberry PI (que se ejecuta con mono). Si los fragmentos de memoria no asignados utilizan demasiada RAM del sistema, el sistema operativo Linux puede volverse inestable. Tengo una aplicación donde tengo que llamar a GC cada segundo (!) Para deshacerme de los problemas de desbordamiento de memoria.

Otra buena solución es deshacerse de los objetos cuando ya no son necesarios. Lamentablemente, esto no es tan fácil en muchos casos.

harry4516
fuente
0

Como hay montón de objetos pequeños (SOH) y montón de objetos grandes (LOH)

Podemos llamar a GC.Collect () para borrar el objeto de desreferencia en SOP y mover el objeto vivido a la próxima generación.

En .net4.5, también podemos compactar LOH usando el modo de compactación de objetos grandes

Max CHien
fuente
0

Si está creando muchos System.Drawing.Bitmapobjetos nuevos , el recolector de basura no los borra. Finalmente, GDI + pensará que se está quedando sin memoria y lanzará una excepción "El parámetro no es válido". Llamar de GC.Collect()vez en cuando (¡no con demasiada frecuencia!) Parece resolver este problema.

Nacht
fuente