2

I have a C# WinForms application where pressing a button instantiates an object, subscribes to its events, then launches a thread based on a method of that object. The object's method uses a lot of memory, but once the thread is finished, I would think it should be released when GC.Collect() is called. However, this does not seem to be the case. This program can use up to gigabytes of memory so this is not a small concern, and it only seems to be released upon closing the program or pressing the button again.

Here is a sample app that demonstrates the problem:

using System;
using System.Threading;
using System.Windows.Forms;

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

        private Worker worker;

        private void button1_Click(object sender, EventArgs e)
        {
            worker = new Worker();
            worker.Finished += worker_Finished;

            Thread thread = new Thread(new ThreadStart(worker.DoWork));
            thread.IsBackground = true;
            thread.Start();
        }

        void worker_Finished(object sender, EventArgs e)
        {
            worker.Finished -= worker_Finished;
            GC.Collect();
            MessageBox.Show((Environment.WorkingSet / 1024 / 1024).ToString() + " MB in use");
        }
    }

    public class Worker
    {
        public event EventHandler Finished;
        protected virtual void OnFinished(EventArgs e)
        {
            EventHandler handler = Finished;
            if(handler != null)
            {
                handler(this, e);
            }
        }

        public void DoWork()
        {
            Random random = new Random();
            string[] list = new string[10000000];
            for(int i = 0; i < list.Length; i++)
            {
                list[i] = random.NextDouble().ToString();
            }
            //list = null;
            OnFinished(new EventArgs());
        }
    }
}

Note that in this case, uncommenting the line list = null; seems to resolve the issue, though I'm not sure why. In my actual application, I have tried setting all the big objects to null before the function ends but it doesn't seem to help. So it's not a perfect recreation of the problem but hopefully somebody can explain what is happening here which will help me solve the actual problem.

Also, I'm aware this question is very similar, but in my case I'm explicitly forcing a garbage collection.

Community
  • 1
  • 1
Craig W
  • 4,390
  • 5
  • 33
  • 51
  • It is just not the way that memory management works. You allocated *virtual memory*. It doesn't cost anything, it is virtual. It makes no sense for the CLR to release memory that doesn't cost anything. It keeps it around, adding it to the list of freed chunks of VM, assuming that you'll pull this trick again. If you don't, and keep allocating memory that doesn't require that freed VM then it *does* release it back. Eventually, you didn't get there yet with this test program. – Hans Passant Jan 31 '14 at 02:10
  • My concern is the program shows it is using several GB of memory in Task Manager long after it has finished its processing. This is fairly alarming, even if it's just a superficial issue. Is there no way to resolve it? – Craig W Jan 31 '14 at 04:19
  • Managed code should not "finish", it needs to keep allocating memory to get the garbage collector to run. As long as that doesn't happen, there is no reason for the CLR to change its VM allocations. – Hans Passant Jan 31 '14 at 10:14

3 Answers3

3

The memory in question is being released, it's just doesn't show in the Task Manager or Environment.WorkingSet

There's a difference between Working Set and Private Bytes memory (more here). Private Bytes is what your process actually uses and Working Set also contain memory that "sits" in your process but can be used by others.

To see only Private Bytes add the specific column in the Task Manager or better, use Performance Monitor.


It's also true that without //list = null; the list still holds a reference to the array while you check Environment.WorkingSet but that's relevant to the pseudo code more than the problem itself.

Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • What is the name of the column I should add in Task Manager to see Private Bytes? The closest I see is Memory (Private Working Set). – Craig W Jan 31 '14 at 04:17
  • `list` should fall out of scope and be eligible for GC after the thread finishes though right? I also tried setting thread to null in the `worker_Finished` method in case there was still a reference to `list` through that but no change. – Craig W Jan 31 '14 at 04:21
  • @CraigW private working set is fine. For a more robust tool use performance monitor. it would be eligible, just remember that the event is still being run on that thread. Also, GC.Collect doesn't **really** force the freeing of that list. To really test it run the same thing over time and see if your memory increases or doesn't. – i3arnon Jan 31 '14 at 12:32
3

Garbage collection is a complex affair. It's not just a case of reclaiming all the memory used. There are complex relationships between objects to consider so the GC has to make sure that it doesn't clean up an object that can still be accessed somewhere.

Because you are raising the event before the DoWork method completes, the 'list' variable is still in scope, so its contents cannot be cleaned up when you call Collect. By setting the variable to null you remove the reference to the array and therefore all the String objects it refers to, so all their memory can be reclaimed by the call to Collect.

jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
0

Try this scheme. Put the actual work in a private method, and have the public method call it, and when that returns throw the OnFinished event (see note), this avoids having to implicitly set variables to null since they will lose scope in DoActualWork()

public class Worker
{
    public event EventHandler Finished;
    protected virtual void OnFinished(EventArgs e)
    {
        EventHandler handler = Finished;
        if(handler != null)
        {
            handler(this, e);
        }
    }

    private void DoActualWork() {
        Random random = new Random();
        string[] list = new string[10000000];
        for(int i = 0; i < list.Length; i++)
        {
            list[i] = random.NextDouble().ToString();
        }            
    }

    public void DoWork()
    {
        DoActualWork();
        OnFinished(EventArgs.Empty); // This is preferred.
    }
}

Like what jmcilhinney said, the GC.Collect() is getting called while the list is still in scope and thus is never collected. By separating the calling of the finish event from the actual work method, you completely avoid it.

Moop
  • 3,414
  • 2
  • 23
  • 37