为什么即使强制垃圾收集完成也未完全释放的线程使用的内存?

时间:2014-01-31 01:26:51

标签: c# .net multithreading memory garbage-collection

我有一个C#WinForms应用程序,按下按钮实例化一个对象,订阅它的事件,然后根据该对象的方法启动一个线程。对象的方法使用了大量内存,但是一旦线程完成,我认为应该在调用GC.Collect()时释放它。但是,情况似乎并非如此。这个程序可以使用高达几千兆字节的内存,因此这不是一个小问题,它似乎只是在关闭程序或再次按下按钮时释放。

以下是演示此问题的示例应用:

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());
        }
    }
}

请注意,在这种情况下,取消注释行list = null;似乎可以解决问题,但我不确定原因。在我的实际应用程序中,我尝试在函数结束之前将所有大对象设置为null,但它似乎没有帮助。所以这不是问题的完美再现,但希望有人可以解释这里发生的事情,这将有助于我解决实际问题。

另外,我知道this question非常相似,但在我的情况下,我明确强制进行垃圾收集。

3 个答案:

答案 0 :(得分:3)

有问题的内存 正在发布,它只是没有显示在任务管理器中或Environment.WorkingSet

Working SetPrivate Bytes内存之间存在差异(更多here)。 Private Bytes是您的流程实际使用的内容,Working Set也包含在您的流程中“处于”但可供其他人使用的内存。

要仅查看Private Bytes添加Task Manager或更高版本中的特定列,请使用Performance Monitor


同样正确的是,如果没有//list = null;list仍会保留对数组的引用,而您检查Environment.WorkingSet但这与伪代码的关系比问题本身更多。

答案 1 :(得分:3)

垃圾收集是一件复杂的事情。这不仅仅是回收所有使用的内存的情况。要考虑的对象之间存在复杂的关系,因此GC必须确保它不会清理仍然可以在某处访问的对象。

因为您在DoWork方法完成之前引发事件,所以'list'变量仍在范围内,因此在调用Collect时无法清除其内容。通过将变量设置为null,可以删除对数组的引用,从而删除它引用的所有String对象,因此可以通过调用Collect来回收所有内存。

答案 2 :(得分:0)

试试这个方案。将实际工作放在私有方法中,并让公共方法调用它,当返回抛出OnFinished事件(请参阅注释)时,这避免了必须隐式设置变量null,因为它们将失去DoActualWork()的范围{1}}

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.
    }
}

就像jmcilhinney所说的那样,当列表仍然在范围内时GC.Collect()被调用,因此永远不会被收集。通过将完成事件的调用与实际工作方法分开,您可以完全避免它。