Comprender la recolección de basura en .NET

170

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, aunque la variable c1 en el método principal está fuera del alcance y ningún otro objeto hace referencia a ella cuando GC.Collect()se llama, ¿por qué no se finaliza allí?

Victor Mukherjee
fuente
8
El GC no libera instancias inmediatamente cuando están fuera del alcance. Lo hace cuando cree que es necesario. Puede leer todo sobre el GC aquí: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061
@ user1908061 (Pssst. Su enlace está roto.)
Dragomok

Respuestas:

352

Te están tropezando aquí y sacas conclusiones muy erróneas porque estás usando un depurador. Tendrá que ejecutar su código de la forma en que se ejecuta en la máquina de su usuario. Cambie a la versión Release primero con Build + Configuration manager, cambie el combo "Configuración de solución activa" en la esquina superior izquierda a "Release". A continuación, vaya a Herramientas + Opciones, Depuración, General y desactive la opción "Suprimir la optimización de JIT".

Ahora ejecute su programa nuevamente y juegue con el código fuente. Tenga en cuenta que las llaves adicionales no tienen ningún efecto. Y observe cómo establecer la variable en nulo no hace ninguna diferencia. Siempre imprimirá "1". Ahora funciona de la manera que esperaba y esperaba que funcionara.

Lo que deja con la tarea de explicar por qué funciona tan diferente cuando ejecuta la compilación de depuración. 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, la fluctuación de fase realiza dos tareas importantes cuando compila 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 la máquina con la ventana Depuración + Windows + Desmontaje. Sin embargo, el segundo deber es 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 variable local con dos direcciones. La dirección donde la variable almacenará primero 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 pila 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 recolecció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 mesa 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 recopilar una referencia de objeto, incluso si se usa dentro de un método y ese método aún no ha terminado de ejecutarse. Lo cual es muy común, su método Main (), por ejemplo, solo dejará de ejecutarse justo antes de que finalice su programa. Claramente, no querrá que ninguna referencia de objeto utilizada dentro de ese método Main () viva durante la duración del programa, eso equivaldría a una fuga. El jitter puede usar la tabla para descubrir que dicha variable local ya no es útil, dependiendo de cuánto haya progresado el programa 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. Se extiendela vida útil de la variable local, evitando que la referencia que almacena se recolecte basura. El único momento en que necesita usarlo es evitar que el GC esté demasiado ansioso por recopilar una referencia, lo que puede suceder en escenarios de interoperabilidad en los que se pasa una referencia a código no administrado. El recolector de basura no puede ver tales referencias siendo utilizadas por dicho código ya que no fue compilado por el jitter, por lo que no tiene la tabla que dice dónde buscar la referencia. Pasar un objeto delegado a una función no administrada como EnumWindows () es el ejemplo de cuando necesitas usar GC.KeepAlive ().

Entonces, como puede deducir de su fragmento de muestra después de ejecutarlo en la compilación de lanzamiento, las variables locales se pueden recopilar temprano, antes de que el método termine de ejecutarse. Aún más poderoso, 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 poner la variable en la ventana Observar o inspeccionarla. Y desaparecería mientras está depurando si se produce un GC. Eso sería muy desagradable, por lo que la inquietud es consciente de que hay un depurador adjunto. Luego modificala tabla y altera la dirección del "último uso". Y lo cambia de su valor normal a la dirección de la última instrucción del método. Lo que mantiene viva la variable mientras el método no haya regresado. Lo que le permite seguir viéndolo hasta que el método regrese.

Esto ahora también explica lo que viste antes y por qué hiciste la pregunta. Imprime "0" porque la llamada GC.Collect no puede recopilar la referencia. La tabla dice que la variable está en uso después de la llamada GC.Collect (), hasta el final del método. Obligado a decir eso al tener el depurador conectado y al ejecutar la compilación de depuración.

Establecer la variable en 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 programadores de C # han caído, en realidad escribir ese código no tenía sentido. No importa en absoluto si esa declaración está presente o no cuando ejecuta el código en la compilación de lanzamiento. De hecho, el optimizador de jitter eliminará esa declaración ya que no tiene ningún efecto. Así que asegúrese de no escribir código como ese, aunque parezca tener un efecto.


Una nota final sobre este tema, esto es lo que causa problemas a los programadores que escriben pequeños programas para hacer algo con una aplicación de Office. El depurador generalmente los pone en el camino equivocado, quieren que el programa 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, llevándolos a la tierra de nunca jamás llamando a Marshal.ReleaseComObject (). Gestión de memoria manual, rara vez funciona correctamente porque pasarán por alto fácilmente una referencia de interfaz invisible. GC.Collect () realmente funciona, pero no cuando depura la aplicación.

Hans Passant
fuente
1
Vea también mi pregunta que Hans respondió muy bien por mí. stackoverflow.com/questions/15561025/…
Dave Nay
1
@HansPassant Acabo de encontrar esta increíble explicación, que también responde parte de mi pregunta aquí: stackoverflow.com/questions/30529379/… sobre GC y sincronización de subprocesos. Una pregunta que aún tengo: me pregunto si el GC realmente compacta y actualiza las direcciones que se usan en un registro (almacenadas en la memoria mientras están suspendidas), o simplemente las omite. Un proceso que actualiza los registros después de suspender el hilo (antes del currículum) me parece un hilo de seguridad serio que está bloqueado por el sistema operativo.
atlaste
Indirectamente sí. El hilo se suspende, el GC actualiza el almacén de respaldo para los registros de la CPU. Una vez que se reanuda la ejecución del subproceso, ahora utiliza los valores de registro actualizados.
Hans Passant
1
@HansPassant, agradecería si agrega referencias para algunos de los detalles no obvios del recolector de basura CLR que describió aquí.
denfromufa
Parece que en cuanto a la configuración, un punto importante es que "Optimizar código" ( <Optimize>true</Optimize>in .csproj) está habilitado. Este es el valor predeterminado en la configuración "Release". Pero en caso de que se usen configuraciones personalizadas, es relevante saber que esta configuración es importante.
Zero3
34

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

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

BREVE CONCEPTOS ::

  1. Objetos que NO implementan Finalizemétodos, la memoria se recupera de inmediato, a menos que, por supuesto, ya no sean alcanzables por
    código de aplicación

  2. Implementar objetos de Finalizemétodo, el concepto / Implementación de Application Roots, Finalization Queue, Freacheable Queueviene antes de que puedan ser recuperados.

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

Supongamos que: las clases / objetos A, B, D, G, H NO implementan el FinalizeMétodo y C, E, F, I, J implementan el FinalizeMétodo.

Cuando una aplicación crea un nuevo objeto, el nuevo operador asigna la memoria del montón. Si el tipo del objeto contiene un Finalizemétodo, 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 Finalizemétodo llamado antes de que la memoria del objeto pueda ser reclamada. 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 lo son. Cuando se crearon los objetos C, E, F, I y J, el marco .Net detecta que estos objetos tienen Finalizemétodos y se agregan punteros a estos objetos a la cola de finalización .

ingrese la descripción de la imagen aquí

Cuando se produce un GC (primera 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 Aplicación representado a través de las flechas del Cuadro amarillo arriba.

El recolector de basura explora 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 alcanzable ("F-alcanzable").

La cola alcanzable es otra estructura de datos interna controlada por el recolector de basura. Cada puntero en la cola alcanzable identifica un objeto que está listo para que se Finalizellame su método.

Después de la colección (1st Collection), el montón administrado se parece a la figura siguiente. La explicación dada a continuación ::
1.) La memoria ocupada por los objetos B, G y H se ha reclamado de inmediato porque estos objetos no tenían un método de finalización que debiera llamarse .

2.) Sin embargo, la memoria ocupada por los objetos E, I y J no se pudo recuperar porque su Finalizemétodo aún no se ha llamado. La llamada al método Finalize se realiza mediante una cola freacheable.

3.) A, C, D, F todavía son alcanzables por el Código de Aplicación representado a través de flechas del recuadro amarillo arriba, por lo que NO se recopilarán en ningún caso

ingrese la descripción de la imagen aquí

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

La próxima vez que se invoca el recolector de basura (2da colección), ve que los objetos finalizados son realmente basura, ya que las raíces de la aplicación no lo señalan y la cola freachable ya no lo señala (también está VACÍO), por lo tanto, el la memoria para los objetos (E, I, J) simplemente se recupera de Heap. Vea la figura a continuación y compárela con la figura justo arriba

ingrese la descripción de la imagen aquí

Lo importante a entender aquí es que se requieren dos GC para recuperar 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 promoverse a una generación anterior

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

Como última nota, recuerde que la aplicación de depuración es una cosa, Garbage Collection es otra cosa y funciona de manera diferente. Hasta ahora no puede SENTIR la recolección de basura simplemente depurando aplicaciones, más aún si desea investigar Memoria, comience aquí.

RC
fuente