C# 为什么已完成的线程使用的内存即使在强制垃圾收集时也不会被释放?

C# 为什么已完成的线程使用的内存即使在强制垃圾收集时也不会被释放?,c#,.net,multithreading,memory,garbage-collection,C#,.net,Multithreading,Memory,Garbage Collection,我有一个C#WinForms应用程序,其中按下按钮实例化一个对象,订阅其事件,然后基于该对象的方法启动一个线程。对象的方法使用了大量内存,但一旦线程完成,我认为应该在调用GC.Collect()时释放它。然而,情况似乎并非如此。这个程序最多可以使用千兆字节的内存,所以这不是一个小问题,它似乎只有在关闭程序或再次按下按钮时才会被释放 下面是一个示例应用程序,演示了该问题: using System; using System.Threading; using System.Windows.Form

我有一个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());
        }
    }
}
使用系统;
使用系统线程;
使用System.Windows.Forms;
命名空间Windows窗体应用程序1
{
公共部分类Form1:Form
{
公共表格1()
{
初始化组件();
}
私人工人;
私有无效按钮1\u单击(对象发送者,事件参数e)
{
工人=新工人();
worker.Finished+=worker\u Finished;
线程线程=新线程(新线程开始(worker.DoWork));
thread.IsBackground=true;
thread.Start();
}
无效工作线程已完成(对象发送器,事件参数e)
{
worker.Finished-=worker\u Finished;
GC.Collect();
Show((Environment.WorkingSet/1024/1024).ToString()+“MB正在使用”);
}
}
公社工人
{
公共事件事件处理程序已完成;
已完成受保护的虚拟空间(事件参数e)
{
EventHandler=已完成;
if(处理程序!=null)
{
处理者(本,e);
}
}
公共工作
{
随机=新随机();
字符串[]列表=新字符串[10000000];
for(int i=0;i
注意,在这种情况下,取消注释行
list=null似乎解决了这个问题,但我不知道为什么。在我的实际应用程序中,我尝试在函数结束之前将所有大对象设置为null,但似乎没有帮助。所以这并不是对问题的完美再现,但希望有人能解释这里发生了什么,这将帮助我解决实际问题


另外,我知道这非常相似,但在我的例子中,我显式地强制执行垃圾收集。

相关内存正在释放,它只是没有显示在任务管理器或
环境中。WorkingSet

工作集
专用字节
内存(更多)之间存在差异
Private Bytes
是进程实际使用的,并且
Working Set
还包含“驻留”在进程中但可供其他人使用的内存

要仅查看
专用字节
任务管理器中添加特定列
或更好,请使用
性能监视器



同样,如果没有
//list=null
在检查
环境时,
列表仍然保留对数组的引用。WorkingSet
,但这与伪代码的关系大于问题本身。

垃圾收集是一件复杂的事情。这不仅仅是回收所有使用的内存。要考虑的对象之间有复杂的关系,所以GC必须确保它不清理一个仍然可以在某处访问的对象。
由于在DoWork方法完成之前引发事件,“list”变量仍在作用域中,因此在调用Collect时无法清除其内容。通过将变量设置为null,可以删除对数组的引用,从而删除它所引用的所有字符串对象,这样就可以通过调用Collect来回收它们的所有内存。

尝试此方案。将实际工作放在一个私有方法中,并让公共方法调用它,当该方法返回throw OnFinished事件(请参见注释)时,这避免了必须将变量隐式设置为
null
,因为它们将在
DoActualWork()中失去作用域。

公共类工作者
{
公共事件事件处理程序已完成;
已完成受保护的虚拟空间(事件参数e)
{
EventHandler=已完成;
if(处理程序!=null)
{
处理者(本,e);
}
}
私人无效DoActualWork(){
随机=新随机();
字符串[]列表=新字符串[10000000];
for(int i=0;i

正如jmcillhinney所说,当列表仍在作用域中时会调用GC.Collect(),因此永远不会被收集。通过将finish事件的调用与实际工作方法分离,可以完全避免它。

这不是内存管理的工作方式。您分配了虚拟内存。它不需要任何费用,它是虚拟的。CLR释放不需要任何成本的内存是没有意义的。它保留它,将它添加到VM的已释放块列表中,假设您将再次使用此技巧。如果您不这样做,并且继续分配不需要释放的VM的内存,那么它会将其释放回来。最终,这个测试程序还没有达到目标。我关心的是,该程序在任务管理器中使用了几GB的内存,而任务管理器已经完成了它的处理。这相当令人担忧,即使这只是一个表面问题。没有办法解决它吗?托管代码不应该“fi”
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.
    }
}