Garbage collection working sometimes and not others...?

G

Guest

Hi, I am using Team Foundation Server's unit testing features and I have
discovered something that is causing me a bit of a problem: please take a
look at the following test code:

public void UnloadAnyControlTest()
{
Form form = new Form();

Panel panel = new Panel();

WeakReference panelRef = new WeakReference(panel);

panel.Dock = DockStyle.Fill;

// Below are the two lines of code which, if commented out,
// will permit this test to pass. Otherwise, it fails.
form.Controls.Add(panel);
form.Controls.Remove(panel);

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

Assert.IsNotNull(panelRef.Target);

panel = null;

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

Assert.IsNull(panelRef.Target);
}

Basically, I have some user controls that I developed and I was writing some
test code to ensure that they were properly cleaned up when I unloaded them.
I found that it wasn't happening, and I wrote the above test to see if I
could figure out what was going on.

Basically, if I create a panel and create a weak reference to it, then null
out (destroy) the only other reference I have to the panel, the garbage
collector will clean up the panel. If, however, in between there I add the
panel to a form and then remove it again, the panel will not be cleaned up!

Does anyone know why?

Can anyone suggest a solution or shed some light on this issue?

Thanks in advance,

-John
 
N

Norman Yuan

See comment inline.


Hassiah said:
Hi, I am using Team Foundation Server's unit testing features and I have
discovered something that is causing me a bit of a problem: please take a
look at the following test code:

public void UnloadAnyControlTest()
{
Form form = new Form();

Panel panel = new Panel();

WeakReference panelRef = new WeakReference(panel);

panel.Dock = DockStyle.Fill;

// Below are the two lines of code which, if commented out,
// will permit this test to pass. Otherwise, it fails.
form.Controls.Add(panel);
form.Controls.Remove(panel);

You remove the panel object from the form's Controls collection, but the
Panel object is still alive and have a reference (variable "panel") pointing
to it. So, it cannot be GCed. Thus following call to GC.Collec() does
nothing to the Panel object.
GC.Collect();
GC.WaitForPendingFinalizers();

Assert.IsNotNull(panelRef.Target);

panel = null;

Now, since the variable "panel" is pointing to null, i.e. it no longer
points to the Panel object instance, the Panel object instance is now
eligible for garbage collecting, if there is no other reference pointing to
it (in your case, there is no). You do not have to call GC.Collect() to
clean up here. GC will clean it up in an supposed optimized manner. Unless
the panel takes huge resources (memory) and you want to release them for
immediately following memory-hungary process.
 
C

Carl Daniel [VC++ MVP]

Norman said:
See comment inline.

You misinterpreted the OPs code, see inline.
You remove the panel object from the form's Controls collection, but
the Panel object is still alive and have a reference (variable
"panel") pointing to it. So, it cannot be GCed. Thus following call
to GC.Collec() does nothing to the Panel object.

....which is exactly what the OP was expecting. Note Assert.IsNotNull in the
next lines of code.
Now, since the variable "panel" is pointing to null, i.e. it no longer
points to the Panel object instance, the Panel object instance is now
eligible for garbage collecting, if there is no other reference
pointing to it (in your case, there is no).

.... which is also what the OP was expecting. Note the Assert.IsNull in the
next lines of code.


You do not have to call
GC.Collect() to clean up here. GC will clean it up in an supposed
optimized manner. Unless the panel takes huge resources (memory) and
you want to release them for immediately following memory-hungary
process.

The whole point of the exercise was to construct a scenario in which an
object _should_ be collected to test that the object was disposed properly.

-cd
 
C

Carl Daniel [VC++ MVP]

Hassiah said:
Hi, I am using Team Foundation Server's unit testing features and I
have discovered something that is causing me a bit of a problem:
please take a look at the following test code:

public void UnloadAnyControlTest()
{
Form form = new Form();

Panel panel = new Panel();

WeakReference panelRef = new WeakReference(panel);

panel.Dock = DockStyle.Fill;

// Below are the two lines of code which, if commented out,
// will permit this test to pass. Otherwise, it fails.
form.Controls.Add(panel);
form.Controls.Remove(panel);

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

Assert.IsNotNull(panelRef.Target);

panel = null;

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

Assert.IsNull(panelRef.Target);
}

Basically, I have some user controls that I developed and I was
writing some test code to ensure that they were properly cleaned up
when I unloaded them. I found that it wasn't happening, and I wrote
the above test to see if I could figure out what was going on.

Basically, if I create a panel and create a weak reference to it,
then null out (destroy) the only other reference I have to the panel,
the garbage collector will clean up the panel. If, however, in
between there I add the panel to a form and then remove it again, the
panel will not be cleaned up!

Does anyone know why?

The problem is simple, I believe: GC.Collect is not guaranteed to do
anything at all. Basically, you're saying "GC, it's OK to collect now".
The GC is free to ignore your request for any reason whatsoever.

Adding and removing the panel to the form probably just generated enough
additional Gen0 garbage to actually trigger a collect.
Can anyone suggest a solution or shed some light on this issue?

To test your control's clean up, forcibly call it's Dispose method and
verify that the right things happen. Beyond that, you have to rely on the
runtime to (eventually) clean up your control in real use.

If your control is holding an expensive resource, you may need to provide a
means to deterministically release that resource. Typically, however,
that's not necessary if you've followed proper IDisposable hygiene
everywhere (in particular, Dispose of your forms when you're done with
them - that will, in turn, Dispose all of the controls on the form).

-cd
 
G

Guest

Hi Carl,

Thanks for your reply. It was helpful... let me, however, give you a bit
more background.

1. I do not plan on ever calling GC.Collect in my code.

2. I am writing a large WinForms app, and the main form will have 5 or 6
different "screens" or "modes" that it can be in. I plan on writing each
"screen" as a user control that will contain sub-controls, and so on. When
another screen/mode is selected, I want to unload the first screen and load
the second screen into the main form. I want to be sure that the runtime is
prepared to free the memory and resources used by the first screen so that my
app doesn't suck up a ton of memory. That was the reason for my test.

3. I understand that GC.Collect does not necessarily do anything (although
I do believe the documentation says that it "forces garbage collection.").
However, I find it very curious that every single time I run the test without
adding the panel to a form, it gets cleaned, and every time I run it with
adding the panel to a form, it does NOT get cleaned. I spent some time using
a tool called Reflector to try to see what happens when a control is added to
a form, but apart from MDIControls, I could not find anywhere where a
reference gets created from the parent form to the child control (panel in
this case).

So, I am not fully satisfied yet... but thanks for the information you gave;
I did find it insightful and helpful.

-John
 
M

Mattias Sjögren

Does anyone know why?

Winforms itself holds references to forms and controls as long as the
underlying HWND is valid.

Does the test pass if you call panel.Dispose() after it has been
removed from the controls collection?



Mattias
 
G

Guest

I had already tried that... I tried it again just for fun... right after the
call to:

form.Controls.Add(panel);
form.Controls.Remove(panel);

I put in a call to:

panel.Dispose();

Again, as before, the call to Assert.IsNotNull passes but the call to
Assert.IsNull (at the end) fails.

This is strange to me, but I invite others to try it themselves. It is a
bit disconcerting to me that windows will not clean up an object that I no
longer have any use for...
 
G

Guest

All-

I had also posted my perplexing question on the framework.windowsforms
group. For a while, I got no replies over there, but this morning a
wonderful person had psoted this (below)... I think it is the solution. If I
try it and it doesn't work, I will advise, but otherwise I think this is the
key. Thanks everyone... thought you might like to know.

Solution from Stoitcho Goutsev (100)-----------------------

John,

This problem (that now I'm confident to say it's a bug in WindowsForms)
bugged me since I saw your post. I was running your sample and I was getting
the same wrong results over and over again and couldn't figure out why. I
spent hours digging with the reflector in the windows forms code and
everything looked OK; there was no leaking references to the panel, so it
shouldn't have happened.

Finally I created my own test application and all of a sudden it was working
coorectly. Then I compared yours sample with mine and I found that the
difference was that I didn't set the panel's Dock property. That was it! If
the panel is docked when removed, something keeps reference to it; if is not
docked it gets GC-ed correctly. Knowing where to look at, I finaly found the
reason. The layout engine caches in each controls the bounds of all docked
child controls. The thing is that it does that using the control as a key in
a dictionary. The problem is that when you remove the docked panel nothing
clears the cache and the cotrol is still referenced as a key in the
dictionary.

There are two workarounds that I found:

Workaround 1:
Before removing the panel form the form set the panel's Dock to None and
then remove it.

Workaround2:
I found that the cache is cleared during layout, thus the solution is to
call form's PerformLayout method after removing the panel.

In your sample code add:
form.PerformLayout()
right after the line : panel = null;

Sometimes the layout is triggered by itself e.g. if there are other docked
controls, but in your case there is none.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top