简体   繁体   中英

Why is memory used by a thread that completed not being released even upon forced garbage collection?

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.

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.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM