【发布时间】:2010-09-19 00:22:27
【问题描述】:
根据我的经验,似乎大多数人会告诉您强制进行垃圾收集是不明智的,但在某些情况下,您正在处理大型对象,这些对象并不总是在 0 代中被收集,但内存是一个问题,可以强制收集吗?是否有这样做的最佳做法?
【问题讨论】:
标签: c# .net garbage-collection
根据我的经验,似乎大多数人会告诉您强制进行垃圾收集是不明智的,但在某些情况下,您正在处理大型对象,这些对象并不总是在 0 代中被收集,但内存是一个问题,可以强制收集吗?是否有这样做的最佳做法?
【问题讨论】:
标签: c# .net garbage-collection
我学会了不要试图智取垃圾收集。话虽如此,在处理文件 I/O 或数据库连接等非托管资源时,我还是坚持使用 using 关键字。
【讨论】:
不确定这是否是最佳实践,但在循环处理大量图像时(即创建和处理大量 Graphics/Image/Bitmap 对象),我经常让 GC.Collect。
我想我在某处读到过,GC 仅在程序(大部分)空闲时运行,而不是处于密集循环的中间,因此这看起来像是手动 GC 可能有意义的区域。
【讨论】:
我认为您已经列出了最佳实践,除非真的有必要,否则不要使用它。我强烈建议您更详细地查看您的代码,如果需要先回答这些问题,可以使用分析工具。
【讨论】:
最佳做法是不要强制进行垃圾回收。
根据 MSDN:
"可以强制垃圾 通过调用 Collect 来收集,但是 大多数时候,这应该是 避免,因为它可能会产生 性能问题。 "
但是,如果您可以可靠地测试您的代码以确认调用 Collect() 不会产生负面影响,那么请继续...
请确保在您不再需要对象时将其清理干净。如果您有自定义对象,请查看使用“using 语句”和 IDisposable 接口。
此链接在释放内存/垃圾收集等方面提供了一些很好的实用建议:
【讨论】:
TargetOfInvocationNullException。
大对象是在 LOH(大对象堆)上分配的,而不是在 gen 0 上。如果您说它们不会在 gen 0 上进行垃圾收集,那么您是对的。我相信只有在完整的 GC 周期(第 0、1 和 2 代)发生时才会收集它们。
话虽如此,我相信另一方面,当您处理大型对象并且内存压力上升时,GC 会更积极地调整和收集内存。
很难说是否收集以及在什么情况下收集。我曾经在处理带有大量控件等的对话框窗口/表单后执行 GC.Collect() (因为由于创建了许多业务对象实例/加载了大量数据,表单及其控件最终进入第 2 代 - 不显然是大物体),但实际上并没有注意到长期这样做的任何积极或消极影响。
【讨论】:
假设您的程序没有内存泄漏,对象会累积并且无法在 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 可能不会提高程序的性能。很可能使情况变得更糟。
.NET GC 经过精心设计和调整以具有自适应性,这意味着它可以根据您程序内存使用的“习惯”调整 GC0/1/2 阈值。因此,它会在运行一段时间后适应您的程序。一旦您显式调用 GC.Collect,阈值将被重置!而且.NET 必须花时间重新适应你的程序的“习惯”。
我的建议是始终信任 .NET GC。如果出现任何内存问题,请检查“.NET Memory”性能计数器并诊断我自己的代码。
【讨论】:
这样看——厨房垃圾是在垃圾桶有10%的时候扔掉,还是先装满再拿出来?
不让它填满,就是在浪费时间往返外面的垃圾箱。这类似于 GC 线程运行时发生的情况——所有托管线程在运行时都被挂起。而且如果我没记错的话,GC线程可以在多个AppDomain之间共享,所以垃圾回收会影响所有的。
当然,您可能会遇到短期内不会向垃圾桶添加任何东西的情况 - 例如,如果您要休假。那么,出门前把垃圾扔掉是个好主意。
这可能是强制 GC 有帮助的一次 - 如果您的程序空闲,正在使用的内存不会被垃圾回收,因为没有分配。
【讨论】:
但是,如果您可以可靠地测试您的代码以确认调用 Collect() 不会产生负面影响,那么请继续...
恕我直言,这类似于说“如果你能证明你的程序将来永远不会有任何错误,那么继续......”
说真的,强制 GC 对于调试/测试目的很有用。如果你觉得你需要在其他任何时候这样做,那么要么你错了,要么你的程序构建错误。无论哪种方式,解决方案都不会强制 GC...
【讨论】:
我认为Rico Mariani 给出的示例很好:如果应用程序的状态发生重大变化,触发GC 可能是合适的。例如,在文档编辑器中,当文档关闭时触发 GC 可能是可以的。
【讨论】:
编程中很少有绝对的通用准则。有一半的时候,当有人说“你做错了”时,他们只是在宣扬一定数量的教条。在 C 语言中,它曾经担心诸如自修改代码或线程之类的事情,在 GC 语言中,它正在强制 GC 或阻止 GC 运行。
与大多数指南和良好的经验法则(以及良好的设计实践)一样,在极少数情况下围绕既定规范工作确实有意义。您确实必须非常确定您了解案例,您的案例确实需要废除常规做法,并且您了解可能导致的风险和副作用。但也有这样的情况。
编程问题多种多样,需要灵活的方法。我已经看到了在垃圾收集语言中阻止 GC 有意义的情况以及触发它而不是等待它自然发生的地方是有意义的。 95% 的情况下,这两种情况中的任何一种都是没有正确解决问题的标志。但 20 次中有 1 次,可能有一个有效的理由。
【讨论】:
最好的做法是在大多数情况下不强制进行垃圾回收。(我工作过的每个系统都强制进行了垃圾回收,但如果解决了这些问题,就不需要强制回收了。垃圾收集,并大大加快了系统速度。)
少数情况您比垃圾收集器更了解内存使用情况。这在多用户应用程序或一次响应多个请求的服务中不太可能发生。
但是,在某些批处理类型中,您确实比 GC 了解更多。例如。考虑一个应用程序。
您也许能够进行一个案例(经过仔细的)测试,在处理完每个文件后,您应该强制进行一次完整的垃圾回收。
另一种情况是服务每隔几分钟就会唤醒以处理某些项目,并且在睡眠时不保持任何状态。然后在临睡前强制收集完整的数据可能是值得的。
唯一一次我会考虑强迫 收藏就是当我知道很多的时候 的对象最近已创建 目前很少有物体 参考。
我宁愿有一个垃圾回收 API,因为我可以给它关于这类事情的提示,而不必强迫我自己进行 GC。
【讨论】:
不确定这是否是最佳实践...
建议:不确定时不要执行此操作或任何操作。在已知事实时重新评估,然后在性能测试之前/之后进行验证。
【讨论】:
我最近遇到的一个需要手动调用 GC.Collect() 的情况是,在处理封装在小型托管 C++ 对象中的大型 C++ 对象时,这些对象又可以从 C# 访问。
垃圾收集器从未被调用,因为使用的托管内存量可以忽略不计,但使用的非托管内存量很大。在对象上手动调用 Dispose() 需要我自己跟踪何时不再需要对象,而调用 GC.Collect() 将清除不再引用的所有对象.....
【讨论】:
GC.AddMemoryPressure (ApproximateSizeOfUnmanagedResource),然后在终结器中调用GC.RemoveMemoryPressure(addedSize)。这样垃圾收集器将自动运行,同时考虑到可以收集的非托管结构的大小。 stackoverflow.com/questions/1149181/…
我想补充一点: 调用 GC.Collect() (+ WaitForPendingFinalizers()) 是故事的一部分。 正如其他人正确提到的那样, GC.COllect() 是非确定性收集,由 GC 本身 (CLR) 自行决定。 即使您添加了对 WaitForPendingFinalizers 的调用,它也可能不是确定性的。 从这个 msdn link 中获取代码并使用对象循环迭代为 1 或 2 运行代码。您会发现非确定性的含义(在对象的析构函数中设置断点)。 准确地说,当 Wait..() 只有 1 个(或 2 个)延迟对象时,不会调用析构函数。[Citation reqd.]
如果您的代码正在处理非托管资源(例如:外部文件句柄),您必须实现析构函数(或终结器)。
这是一个有趣的例子:
注意:如果您已经从 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() 进行显式调用。这就是本质! :)
【讨论】:
我不推荐手动垃圾回收。我向您保证,您没有正确处理大型物体。使用 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()
【讨论】: