我有一个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非常相似,但在我的情况下,我明确强制进行垃圾收集。
答案 0 :(得分:3)
有问题的内存 正在发布,它只是没有显示在任务管理器中或Environment.WorkingSet
Working Set
和Private 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()被调用,因此永远不会被收集。通过将完成事件的调用与实际工作方法分开,您可以完全避免它。