Referencias circulares ¿Causa pérdida de memoria?

Estoy tratando de ejecutar una pérdida de memoria en una aplicación de formularios de Windows. Estoy buscando ahora un formulario que contiene varias formas incrustadas. Lo que me preocupa es que los formularios secundarios, en su constructor, hagan referencia al formulario principal y lo mantengan en un campo privado. Entonces me parece que viene el tiempo de recolección de basura:

El padre tiene una referencia al formulario secundario, a través de la colección de controles (el formulario hijo está incrustado allí). La forma del niño no es GC’d.

El formulario hijo tiene una referencia al formulario padre, a través del campo de miembro privado. El formulario principal no es GC.

¿Es esto una comprensión precisa de cómo el recolector de basura evaluará la situación? ¿Alguna forma de “probarlo” para fines de prueba?

Gran pregunta!

No, ambas formas serán (pueden) GC’d porque el GC no busca directamente referencias en otras referencias. Solo busca lo que se llama referencias “Root” … Esto incluye variables de referencia en la stack, (La variable está en la stack, el objeto real está, por supuesto, en el montón), hace referencia a las variables en los registros de la CPU y las variables de referencia que están campos estáticos en clases …

Todas las demás variables de referencia solo son accedidas (y GC’d) si se referencian en una propiedad de uno de los objetos de referencia “raíz” encontrados por el proceso anterior … (o en un objeto referenciado por una referencia en un objeto raíz) , etc …)

Entonces, solo si uno de los formularios es referenciado en otro lugar en una referencia “raíz” – Entonces ambos formularios estarán a salvo del GC.

La única manera en que puedo pensar para “probarlo” (sin usar utilidades de seguimiento de memoria) sería crear cientos de miles de estas formas, en un ciclo dentro de un método, y luego, en el método, observar la huella de memoria de la aplicación , luego salga del método, llame al GC y mire nuevamente la huella.

Como ya han dicho otros, GC no tiene problemas con las referencias circulares. Me gustaría agregar, que un lugar común para filtrar memoria en .NET son los manejadores de eventos. Si uno de sus formularios tiene un controlador de eventos adjunto a otro objeto que está “vivo”, entonces hay una referencia a su formulario y el formulario no recibirá GC’d.

La recolección de basura funciona mediante el seguimiento de las raíces de las aplicaciones. Las raíces de aplicaciones son ubicaciones de almacenamiento que contienen referencias a objetos en el montón administrado (o nulo). En .NET, las raíces son

  1. Referencias a objetos globales
  2. Referencias a objetos estáticos
  3. Referencias a campos estáticos
  4. Referencias en la stack a objetos locales
  5. Las referencias en la stack a los parámetros del objeto pasaron a los métodos
  6. Referencias a objetos esperando ser finalizados
  7. Referencias en registros de CPU a objetos en el montón administrado

La lista de raíces activas es mantenida por CLR. El recolector de basura funciona mirando los objetos en el montón administrado y viendo a los que todavía puede acceder la aplicación, es decir, accesible a través de una raíz de aplicación. Tal objeto se considera rooteado.

Ahora suponga que tiene un formulario principal que contiene referencias a formularios secundarios y estos formularios secundarios contienen referencias al formulario principal. Además, suponga que la aplicación ya no contiene referencias al padre de ninguno de los formularios secundarios ni a ninguno de ellos. Luego, para los propósitos del recolector de basura, estos objetos gestionados ya no se rootean y se recogerán como basura la próxima vez que se produzca una recolección de basura.

Si no se hace referencia al padre y al hijo, sino que solo se referencian entre sí, reciben GCed.

Obtenga un generador de perfiles de memoria para verificar realmente su aplicación y responder a todas sus preguntas. Puedo recomendar http://memprofiler.com/

Me gustaría hacerme eco del comentario de Vilx sobre los eventos y recomendar un patrón de diseño que ayude a abordarlo.

Digamos que tiene un tipo que es un origen de evento, por ejemplo:

interface IEventSource { event EventHandler SomethingHappened; } 

Aquí hay un fragmento de una clase que maneja eventos de instancias de ese tipo. La idea es que cada vez que asigne una nueva instancia a la propiedad, primero se da de baja de cualquier tarea previa y luego se suscribe a la nueva instancia. Las comprobaciones nulas aseguran los comportamientos límite correctos, y más al punto, simplifican la eliminación: todo lo que haces es anular la propiedad.

Lo que trae a colación el punto de eliminación. Cualquier clase que se suscriba a eventos debe implementar la interfaz IDisposable porque los eventos son recursos administrados. (Nota: me salté una implementación adecuada del patrón Dispose en el ejemplo por motivos de brevedad, pero se entiende la idea).

 class MyClass : IDisposable { IEventSource m_EventSource; public IEventSource EventSource { get { return m_EventSource; } set { if( null != m_EventSource ) { m_EventSource -= HandleSomethingHappened; } m_EventSource = value; if( null != m_EventSource ) { m_EventSource += HandleSomethingHappened; } } } public Dispose() { EventSource = null; } // ... } 

El GC puede tratar correctamente las referencias circulares y si estas referencias fueran las únicas que mantuvieran el formulario vivo, entonces serían recolectadas.
He tenido muchos problemas con .net al no recuperar memoria de formularios. En 1.1 hubo algunos errores alrededor de menuitem (creo) lo que significaba que no se eliminaban y podían perder memoria. En este caso, al agregar una llamada explícita para eliminar y borrar la variable miembro en el método Dispose del formulario, se solucionó el problema. Descubrimos que esto también ayudó a reclamar memoria para algunos de los otros tipos de control.
También pasé mucho tiempo con CLR Profiler mirando por qué los formularios no se estaban recostackndo. Por lo que pude ver, el marco estaba manteniendo las referencias. Uno por tipo de formulario. Entonces, si crea 100 instancias de Form1, entonces ciérrelas todas, solo 99 se recuperarán correctamente. No encontré ninguna manera de curar esto.
Nuestra aplicación se ha movido a .net 2 y esto parece ser mucho mejor. La memoria de nuestra aplicación aún aumenta cuando abrimos el primer formulario y no retrocede cuando está cerrado, pero creo que esto se debe al código JIT y a las bibliotecas de control extra que están cargadas.
También encontré que aunque el GC puede tratar con referencias circulares, parece tener problemas (a veces) con referencias circulares de manejadores de eventos. IE object1 hace referencia a object2 y object1 tiene un método que maneja y evento desde object2. Encontré circunstancias en las que esto no liberaba los objetos cuando esperaba, pero nunca pude volver a producirlos en un caso de prueba.