Comprender la recolección de basura en .NET

Considere el siguiente código:

public class Class1 { public static int c; ~Class1() { c++; } } public class Class2 { public static void Main() { { var c1=new Class1(); //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1. } GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine(Class1.c); // prints 0 Console.Read(); } } 

Ahora, a pesar de que la variable c1 en el método principal está fuera del scope y no hace referencia a ningún otro objeto cuando se llama a GC.Collect() , ¿por qué no se finaliza allí?

Te están tropezando aquí y sacando conclusiones muy incorrectas porque estás usando un depurador. Tendrá que ejecutar su código de la misma manera que se ejecuta en la máquina de su usuario. Cambie a la versión de Release primero con Build + Configuration Manager, cambie el combo “Active solution configuration” en la esquina superior izquierda a “Release”. A continuación, vaya a Herramientas + Opciones, Depuración, General y desmarque la opción “Suprimir JIT optimización”.

Ahora ejecute su progtwig de nuevo y juegue con el código fuente. Tenga en cuenta cómo las llaves adicionales no tienen ningún efecto. Y tenga en cuenta cómo establecer la variable a nulo no hace ninguna diferencia. Siempre imprimirá “1”. Ahora funciona de la manera que esperabas y esperaba que funcionaría.

Lo cual deja la tarea de explicar por qué funciona tan diferente cuando ejecuta la comstackción Debug. Eso requiere explicar cómo el recolector de basura descubre las variables locales y cómo se ve afectado por tener un depurador presente.

En primer lugar, el jitter realiza dos tareas importantes cuando comstack el IL para un método en código máquina. El primero es muy visible en el depurador, puede ver el código de máquina con la ventana Depuración + Desassembly de Windows +. El segundo deber es, sin embargo, completamente invisible. También genera una tabla que describe cómo se usan las variables locales dentro del cuerpo del método. Esa tabla tiene una entrada para cada argumento de método y una variable local con dos direcciones. La dirección donde la variable primero almacenará una referencia de objeto. Y la dirección de la instrucción de código de máquina donde esa variable ya no se usa. También si esa variable se almacena en el marco de la stack o en un registro de la CPU.

Esta tabla es esencial para el recolector de basura, necesita saber dónde buscar referencias de objetos cuando realiza una colección. Es bastante fácil de hacer cuando la referencia es parte de un objeto en el montón de GC. Definitivamente no es fácil de hacer cuando la referencia del objeto se almacena en un registro de la CPU. La tabla dice dónde mirar.

La dirección “ya no se usa” en la tabla es muy importante. Hace que el recolector de basura sea muy eficiente . Puede recostackr una referencia de objeto, incluso si se usa dentro de un método y ese método no ha terminado de ejecutarse aún. Lo cual es muy común, su método Main (), por ejemplo, solo dejará de ejecutarse justo antes de que finalice su progtwig. Claramente, no desearía que las referencias a objetos utilizadas dentro de ese método Main () vivieran durante la duración del progtwig, lo que equivaldría a una fuga. El jitter puede usar la tabla para descubrir que tal variable local ya no es útil, dependiendo de cuánto ha progresado el progtwig dentro de ese método Main () antes de realizar una llamada.

Un método casi mágico relacionado con esa tabla es GC.KeepAlive (). Es un método muy especial, no genera ningún código en absoluto. Su único deber es modificar esa tabla. Extiende la vida útil de la variable local, evitando que la referencia que almacena consiga basura recolectada. La única vez que necesita usarlo es evitar que el GC esté demasiado ansioso por recostackr una referencia, lo que puede suceder en escenarios de interoperabilidad en los que se pasa una referencia al código no administrado. El recolector de elementos no utilizados no puede ver esas referencias utilizadas por dicho código, ya que no fue comstackdo por el jitter, por lo que no tiene la tabla que indica dónde buscar la referencia. Pasar un objeto delegado a una función no administrada como EnumWindows () es el ejemplo repetitivo de cuándo debe usar GC.KeepAlive ().

Por lo tanto, como puede ver en el fragmento de muestra después de ejecutarlo en la comstackción Release, las variables locales se pueden recostackr antes de que el método termine de ejecutarse. Aún más poderosamente, un objeto puede ser recolectado mientras uno de sus métodos se ejecuta si ese método ya no se refiere a esto . Hay un problema con eso, es muy incómodo depurar dicho método. Ya que puede colocar la variable en la ventana Inspección o inspeccionarla. Y desaparecería mientras está depurando si ocurre un GC. Eso sería muy desagradable, por lo que el jitter es consciente de que hay un depurador conectado. Luego modifica la tabla y altera la dirección “última vez que se usó”. Y lo cambia de su valor normal a la dirección de la última instrucción en el método. Que mantiene viva la variable siempre que el método no haya regresado. Lo que le permite seguir mirando hasta que regrese el método.

Esto ahora también explica lo que vio antes y por qué hizo la pregunta. Imprime “0” porque la llamada GC.Collect no puede recostackr la referencia. La tabla dice que la variable está en uso más allá de la llamada GC.Collect (), todo el camino hasta el final del método. Obligado a decirlo al tener el depurador conectado y ejecutando la comstackción Debug.

Establecer la variable a nulo tiene un efecto ahora porque el GC inspeccionará la variable y ya no verá una referencia. Pero asegúrese de no caer en la trampa en la que muchos progtwigdores de C # han caído, en realidad escribir ese código no tiene sentido. No importa en absoluto si esa statement está presente cuando ejecuta el código en la versión Release. De hecho, el optimizador de jitter eliminará esa afirmación ya que no tiene ningún efecto en absoluto. Así que asegúrese de no escribir código así, aunque parezca tener un efecto.


Una nota final sobre este tema, esto es lo que hace que los progtwigdores tengan problemas al escribir progtwigs pequeños para hacer algo con una aplicación de Office. Por lo general, el depurador los coloca en la ruta equivocada, quieren que el progtwig de Office salga a pedido. La forma adecuada de hacerlo es llamando a GC.Collect (). Pero descubrirán que no funciona cuando depuran su aplicación, lo que los lleva a la tierra de nunca jamás llamando a Marshal.ReleaseComObject (). La gestión manual de la memoria rara vez funciona correctamente porque pasará fácilmente por alto una referencia de interfaz invisible. GC.Collect () realmente funciona, pero no cuando depura la aplicación.

[Solo quería agregar más sobre el proceso de Internalización de finalización]

Por lo tanto, crea un objeto y cuando se recostack el objeto, debe Finalize método Finalize del objeto. Pero hay más en la finalización que esta suposición muy simple.

CONCEPTOS CORTOS ::

  1. Objetos que NO implementan los métodos de Finalize , la memoria se recupera inmediatamente, a menos que, por supuesto, no puedan volverse a leer por
    código de la aplicación más

  2. Los objetos que implementan Finalize Method, The Concept / Implementation of Application Roots , Finalization Queue , Freacheable Queue antes de que puedan ser recuperados.

  3. Cualquier objeto se considera basura si NO es alcanzable por el código de la aplicación

Asumir :: Clases / Objetos A, B, D, G, H NO implementan el Método de Finalize y C, E, F, I, J implementan el Método de Finalize .

Cuando una aplicación crea un nuevo objeto, el nuevo operador asigna la memoria del montón. Si el tipo del objeto contiene un método Finalize , se coloca un puntero al objeto en la cola de finalización .

por lo tanto, los punteros a los objetos C, E, F, I, J se agregan a la cola de finalización.

La cola de finalización es una estructura de datos interna controlada por el recolector de basura. Cada entrada en la cola apunta a un objeto que debería tener su método Finalize llamado antes de que la memoria del objeto pueda recuperarse. La siguiente figura muestra un montón que contiene varios objetos. Algunos de estos objetos son accesibles desde las raíces de la aplicación , y algunos no. Cuando se crearon los objetos C, E, F, I y J, el framework .Net detecta que estos objetos tienen métodos Finalize y los punteros a estos objetos se agregan a la cola de finalización .

enter image description here

Cuando se produce un CG (1ª colección), se determina que los objetos B, E, G, H, I y J son basura. Debido a que A, C, D, F todavía son alcanzables por el código de la aplicación representado a través de las flechas del cuadro amarillo de arriba.

El recolector de basura escanea la cola de finalización buscando punteros a estos objetos. Cuando se encuentra un puntero, el puntero se elimina de la cola de finalización y se agrega a la cola predecible (“F-alcanzable”).

La cola inalcanzable es otra estructura de datos interna controlada por el recolector de basura. Cada puntero en la cola predecible identifica un objeto que está listo para llamar a su método Finalize .

Después de la colección (1ª colección), el montón administrado tiene un aspecto similar al que se muestra a continuación. Explicación a continuación ::
1.) La memoria ocupada por los objetos B, G y H ha sido reclamada inmediatamente porque estos objetos no tenían un método de finalización que necesitara ser llamado .

2.) Sin embargo, la memoria ocupada por los objetos E, I y J no pudo recuperarse debido a que su método Finalize aún no ha sido llamado. Llamar al método Finalizar se realiza mediante la cola freacheable.

3.) A, C, D, F siguen siendo alcanzables por el Código de aplicación representado a través de las flechas del cuadro amarillo de arriba, por lo que NO se recostackrán en ningún caso

enter image description here

Hay un hilo especial en tiempo de ejecución dedicado a llamar a los métodos Finalize. Cuando la cola localizable está vacía (que generalmente es el caso), este hilo duerme. Pero cuando aparecen las entradas, este subproceso se activa, elimina cada entrada de la cola y llama al método Finalize de cada objeto. El recolector de basura compacta la memoria recuperable y el hilo de tiempo de ejecución especial vacía la cola localizable , ejecutando el método Finalize cada objeto. Así que aquí finalmente es cuando se ejecuta su método Finalize

La próxima vez que se invoca el recolector de basura (2nd Collection), ve que los objetos finalizados son realmente basura, ya que las raíces de la aplicación no apuntan a él y la cola inalcanzable ya no apunta a él (también está VACÍO). Por lo tanto, la memoria de los objetos (E, I, J) simplemente se recupera de Heap. Consulte la figura a continuación y compárela con la figura que se ve arriba.

enter image description here

Lo importante de entender aquí es que se requieren dos GC para reclamar la memoria utilizada por los objetos que requieren finalización . En realidad, incluso se requieren más de dos colecciones, ya que estos objetos pueden ser promovidos a una generación anterior

NOTA :: La cola localizable se considera una raíz al igual que las variables globales y estáticas son las raíces. Por lo tanto, si un objeto está en la cola localizable, entonces el objeto es alcanzable y no es basura.

Como última nota, recuerde que la aplicación de depuración es una cosa, la recolección de basura es otra cosa y funciona de manera diferente. Hasta el momento, no puede SENTIRSE la recolección de basura solo mediante la depuración de aplicaciones, si desea investigar la Memoria, comience aquí.

Hay 3 formas en que puede implementar la administración de memoria:

GC funciona solo para recursos administrados, por lo tanto .NET proporciona Dispose y Finalize para liberar recursos no administrados como stream, conexión a bases de datos, objetos COM, etc.

1) Eliminar

Dispose debe llamarse explícitamente para los tipos que implemente IDisposable.

El progtwigdor debe llamar esto usando Dispose () o mediante Using build

Use GC.SuppressFinalize (esto) para evitar llamar a Finalizer si ya ha utilizado dispose ()

2) Finalize o Distructor

Se llama implícitamente después de que el objeto es elegible para la limpieza, el finalizador para los objetos se llama secuencialmente por el hilo del finalizador.

El inconveniente del finalizador de implementación es que la recuperación de la memoria se retrasa, ya que el finalizador para dicha clase / tipos debe llamarse limpieza previa, por lo que un grupo adicional para reclamar la memoria.

3) GC.Collect ()

El uso de GC.Collect () no necesariamente coloca GC para la recostackción, GC puede anular y ejecutar siempre que lo desee.

también GC.Collect () solo ejecutará la parte de seguimiento de la recolección de elementos no utilizados y agregará elementos a la cola del finalizador, pero no a los finalizadores de llamadas para los tipos, que es manejado por otro hilo.

Use WaitForPendingFinalizers si desea asegurarse de que se hayan llamado a todos los finalizadores después de invocar GC.Collect ()