Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GC not finalizing UserControl?

I have a CF application that over time leaks UserControls. It took some time, but I narrowed it down, and even replicated the behavior in the full framework (3.5). Since the behavior exists in both, I don't want to call it a bug, but I sure don't understand why it's happening and hope someone can shed some light on it.

So I create a simple WinForms app with a Form and a Button. Clicking on the Button alternates between creating a new UserControl and Disposing that control. Very simple.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    UserControl1 m_ctl;

    private void button1_Click(object sender, EventArgs e)
    {
        if (m_ctl == null)
        {
            m_ctl = new UserControl1();
            m_ctl.Visible = true;
            this.Controls.Add(m_ctl);
        }
        else
        {
            this.Controls.Remove(m_ctl);
            m_ctl.Dispose();
            m_ctl = null;
            GC.Collect();
        }
    }
}

And here's the UserControl. It simply tracks the number of live (i.e. not finalized) instances. It has nothing on it but a single label so I can visually confirm it's on the Form.

public partial class UserControl1 : UserControl
{
    private static int m_instanceCount = 0;

    public UserControl1()
    {
        var c = Interlocked.Increment(ref m_instanceCount);
        Debug.WriteLine("Instances: " + c.ToString());

        InitializeComponent();
    }

    ~UserControl1()
    {
        var c = Interlocked.Decrement(ref m_instanceCount);
        Debug.WriteLine("Instances: " + c.ToString());
    }
}

The strange thing here is that the instance count grows indefinitely. Eventually, on the device, I run out of memory. I suspect I would on the PC as well, I'm just not inclined to click the button for the next year.

Now if I alter the UserControl's default, designer-generated Dispose method like this, simply adding the ReRegisterForFinalize call:

protected override void Dispose(bool disposing)
{
    if (disposing && (components != null))
    {
        components.Dispose();
    }

    base.Dispose(disposing);

    if (disposing)
    {
        GC.ReRegisterForFinalize(this);
    }
}

Then it behaves exactly as expected, Finalizing instances during collection (when manual or automatic).

So why is this happening? Evidently the base is calling SuppressFinalize, but exactly why would this be happening, and why in the name of Odin is it the default behavior?

like image 638
ctacke Avatar asked Jun 26 '13 22:06

ctacke


1 Answers

So why is this happening? Evidently the base is calling SuppressFinalize, but exactly why would this be happening, and why in the name of Odin is it the default behavior?

That is the default behavior for classes (properly) implementing IDisposable. When you call IDisposable.Dispose, the default, suggested behavior is to suppress finalization, since the main reason for finalization is to clean up resources that were never disposed. This is because finalization is an expensive operation - you don't want to finalize objects unnecessarily, and if Dispose was called, the thought is that you've already cleaned up your non-managed resources. Any managed memory will get handled regardless.

You should override Dispose, and do your decrement within the Dispose override.

This behavior is explained in the documentation for IDisposable. The sample Dispose method call implementation is (from the referenced documentation):

public void Dispose()
{
    Dispose(true);
    // This object will be cleaned up by the Dispose method. 
    // Therefore, you should call GC.SupressFinalize to 
    // take this object off the finalization queue 
    // and prevent finalization code for this object 
    // from executing a second time
    GC.SuppressFinalize(this);
}
like image 187
Reed Copsey Avatar answered Oct 22 '22 22:10

Reed Copsey