根据我的经验,似乎大多数人会告诉您强制垃圾回收是不明智的,但在某些情况下,您正在处理大型对象,这些对象并不总是在 0 代中被回收,但内存是一个问题,是可以强制收集吗?是否有这样做的最佳实践?
16 回答
最佳做法是不强制进行垃圾收集。
根据 MSDN:
“可以通过调用 Collect 来强制进行垃圾收集,但大多数情况下,应该避免这样做,因为它可能会产生性能问题。”
但是,如果您可以可靠地测试您的代码以确认调用 Collect() 不会产生负面影响,那么请继续......
只需尝试确保在您不再需要对象时将其清理干净。如果您有自定义对象,请查看使用“使用语句”和 IDisposable 接口。
这个链接有一些关于释放内存/垃圾收集等很好的实用建议:
这么看——厨房垃圾是在垃圾桶10%的时候扔掉,还是先装满再拿出来?
不让它填满,你就是在浪费时间往返外面的垃圾箱。这类似于 GC 线程运行时发生的情况——所有托管线程在运行时都被挂起。如果我没记错的话,GC 线程可以在多个 AppDomain 之间共享,因此垃圾收集会影响所有这些。
当然,您可能会遇到一种情况,即您不会在短时间内向垃圾桶添加任何东西 - 例如,如果您要休假。那么,出门前把垃圾扔掉是个好主意。
这可能是强制 GC 可以提供帮助的一次 - 如果您的程序空闲,则正在使用的内存不会被垃圾收集,因为没有分配。
最佳做法是在大多数情况下不强制进行垃圾收集。 (我工作过的每个系统都强制进行垃圾收集,有强调的问题,如果解决了就不需要强制垃圾收集,并且大大加快了系统的运行速度。)
在某些情况下,您比垃圾收集器更了解内存使用情况。在多用户应用程序或一次响应多个请求的服务中,这不太可能是真的。
但是,在某些批处理类型的处理中,您确实比 GC 了解更多。例如,考虑一个应用程序。
- 在命令行上给出文件名列表
- 处理单个文件,然后将结果写入结果文件。
- 在处理文件时,会创建很多相互关联的对象,直到文件处理完成(例如解析树)才能收集这些对象
- 在已处理的文件之间不保留太多状态。
您可能能够进行案例(经过仔细的)测试,在处理完每个文件后,您应该强制进行完整的垃圾回收。
另一种情况是每隔几分钟唤醒一次以处理某些项目的服务,并且在它处于睡眠状态时不保持任何状态。然后在睡觉前强制收集完整的数据可能是值得的。
我会考虑强制收集的唯一一次是当我知道最近创建了很多对象并且当前引用的对象很少时。
我宁愿有一个垃圾收集 API,因为我可以给它提示这种类型的事情,而不必强迫我自己进行 GC。
另见“ Rico Mariani 的表演花絮”
我认为Rico Mariani给出的例子很好:如果应用程序的状态发生重大变化,触发 GC 可能是合适的。例如,在文档编辑器中,当文档关闭时触发 GC 可能是可以的。
编程中很少有绝对的通用准则。有一半的时候,当有人说“你做错了”时,他们只是在宣扬一定数量的教条。在 C 语言中,它曾经担心诸如自修改代码或线程之类的事情,在 GC 语言中,它正在强制 GC 或阻止 GC 运行。
与大多数指南和良好的经验法则(以及良好的设计实践)一样,在极少数情况下围绕既定规范工作确实有意义。您确实必须非常确定您了解案例,您的案例确实需要废除常规做法,并且您了解可能导致的风险和副作用。但也有这样的情况。
编程问题多种多样,需要灵活的方法。我已经看到了在垃圾收集语言中阻止 GC 有意义的情况以及触发它而不是等待它自然发生的地方是有意义的。95% 的情况下,这两种情况中的任何一种都是没有正确解决问题的标志。但是 20 次中有 1 次,可能有一个有效的案例。
我已经学会了不要试图智取垃圾收集。using
话虽如此,在处理文件 I/O 或数据库连接等非托管资源时,我只是坚持使用关键字。
我最近遇到的一个需要手动调用的情况GC.Collect()
是,在处理封装在小型托管 C++ 对象中的大型 C++ 对象时,这些对象又可以从 C# 访问。
垃圾收集器从未被调用,因为使用的托管内存量可以忽略不计,但使用的非托管内存量很大。手动调用Dispose()
对象需要我自己跟踪何时不再需要对象,而调用GC.Collect()
将清除不再引用的任何对象.....
不确定这是否是最佳实践,但是当在循环中处理大量图像时(即创建和处理大量 Graphics/Image/Bitmap 对象),我经常让 GC.Collect。
我想我在某处读到 GC 仅在程序(大部分)空闲时运行,而不是在密集循环的中间,所以这看起来像是手动 GC 可能有意义的区域。
我认为您已经列出了最佳实践,除非真的有必要,否则不要使用它。我强烈建议您更详细地查看您的代码,如果需要先回答这些问题,可以使用分析工具。
- 您的代码中是否有一些东西在比需要的更大范围内声明项目
- 内存使用率真的太高了吗
- 比较使用 GC.Collect() 之前和之后的性能,看看它是否真的有帮助。
假设您的程序没有内存泄漏,对象累积并且无法在 Gen 0 中进行 GC,因为:1)它们被长时间引用,因此进入 Gen1 和 Gen2;2)它们是大对象(> 80K),所以进入LOH(大对象堆)。LOH 不像 Gen0、Gen1 和 Gen2 那样进行压缩。
检查“.NET Memory”的性能计数器可以看出1)问题确实不是问题。一般每 10 次 Gen0 GC 会触发 1 次 Gen1 GC,每 10 次 Gen1 GC 会触发 1 次 Gen2 GC。从理论上讲,如果 GC0 上没有压力(如果程序内存使用确实是有线的),则 GC1 和 GC2 永远不会被 GC-ed。它永远不会发生在我身上。
对于问题 2),您可以检查“.NET Memory”性能计数器来验证 LOH 是否变得臃肿。如果这确实是您的问题,也许您可以创建一个大型对象池,因为此博客建议http://blogs.msdn.com/yunjin/archive/2004/01/27/63642.aspx。
我想补充一点:调用 GC.Collect() (+ WaitForPendingFinalizers()) 是故事的一部分。正如其他人正确提到的那样, GC.COllect() 是非确定性收集,由 GC 本身 (CLR) 自行决定。即使您添加了对 WaitForPendingFinalizers 的调用,它也可能不是确定性的。从此 msdn链接中获取代码,并使用对象循环迭代为 1 或 2 运行代码。您会发现非确定性的含义(在对象的析构函数中设置断点)。准确地说,当 Wait..() 只有 1 个(或 2 个)延迟对象时,不会调用析构函数。 [引用请求]
如果您的代码正在处理非托管资源(例如:外部文件句柄),则必须实现析构函数(或终结器)。
这是一个有趣的例子:
注意:如果您已经尝试过来自 MSDN 的上述示例,那么下面的代码将会清除空气。
class Program
{
static void Main(string[] args)
{
SomePublisher publisher = new SomePublisher();
for (int i = 0; i < 10; i++)
{
SomeSubscriber subscriber = new SomeSubscriber(publisher);
subscriber = null;
}
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(SomeSubscriber.Count.ToString());
Console.ReadLine();
}
}
public class SomePublisher
{
public event EventHandler SomeEvent;
}
public class SomeSubscriber
{
public static int Count;
public SomeSubscriber(SomePublisher publisher)
{
publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
}
~SomeSubscriber()
{
SomeSubscriber.Count++;
}
private void publisher_SomeEvent(object sender, EventArgs e)
{
// TODO: something
string stub = "";
}
}
我建议,首先分析输出可能是什么,然后运行,然后阅读以下原因:
{只有在程序结束时才会隐式调用析构函数。为了确定性地清理对象,必须实现 IDisposable 并对 Dispose() 进行显式调用。这就是本质!:)
大对象是在 LOH(大对象堆)上分配的,而不是在 gen 0 上。如果您说它们不会在 gen 0 上进行垃圾收集,那么您是对的。我相信只有在完整的 GC 周期(第 0、1 和 2 代)发生时才会收集它们。
话虽如此,我相信另一方面,当您处理大型对象并且内存压力上升时,GC 会更积极地调整和收集内存。
是否收集以及在什么情况下收集很难说。我曾经在处理带有大量控件等的对话框窗口/表单后执行 GC.Collect() (因为由于创建了许多业务对象实例/加载了大量数据,表单及其控件最终进入第 2 代 - 不显然是大物体),但实际上并没有注意到长期这样做的任何积极或消极影响。
还有一件事,显式触发 GC Collect 可能不会提高程序的性能。很可能使情况变得更糟。
.NET GC 经过精心设计和调整以具有自适应性,这意味着它可以根据您程序内存使用的“习惯”调整 GC0/1/2 阈值。因此,它会在运行一段时间后适应您的程序。一旦您显式调用 GC.Collect,阈值将被重置!并且.NET 必须花时间再次适应您的程序的“习惯”。
我的建议是始终信任 .NET GC。出现任何内存问题,请检查“.NET Memory”性能计数器并诊断我自己的代码。
不确定这是否是最佳实践...
建议:不确定时不要执行此操作或任何操作。在已知事实时重新评估,然后在性能测试之前/之后进行验证。
但是,如果您可以可靠地测试您的代码以确认调用 Collect() 不会产生负面影响,那么请继续......
恕我直言,这类似于说“如果你能证明你的程序将来永远不会有任何错误,那么继续......”
严肃地说,强制 GC 对于调试/测试目的很有用。如果你觉得你需要在其他任何时候这样做,那么要么你错了,要么你的程序构建错误。无论哪种方式,解决方案都不会强制GC ...
我不推荐手动垃圾收集。我向您保证,您没有正确处理大型物体。使用 USING 语句。每当您实例化一个对象时,请务必在使用完它时将其丢弃。此示例代码使用 USING 语句创建连接。然后它实例化一个运输标签对象,使用它,并正确地处理它。
Using con As SqlConnection = New SqlConnection(DB_CONNECTION_STRING)
con.Open()
Using command As SqlCommand = New SqlCommand(sqlStr, con)
Using reader As SqlDataReader = command.ExecuteReader()
While reader.Read()
code_here()
End While
End Using
End Using
End Using
Dim f1 As frmShippingLabel
f1 = New frmShippingLabel
f1.PrintLabel()
f1.Dispose()