应该定期调用 GC.Collect() 吗?

Should GC.Collect() be called regularly?

我最近发布了一篇关于日志文件 reader 由于内存不足错误而失败的文章 >

在我有机会尝试更简单的方法(将日志文件命名为带有日期的名称以防止归档)之前,这显然意味着重写方法等,我首先尝试了垃圾收集选项,因为我从未使用过它,例如 GC.Collect()。

如果在尝试读取日志文件内容时出现内存错误并且它似乎释放了一半内存,例如在此过程中使用的调试文件(如日志文件显然失效了,所以这是为了帮助我事后调试)我从昨晚的归档过程中得到了这个响应。

Attempt Archive
Take contents of current file with ReadFileString  (this is my custom stream reader method I wrote which you can see in the original article)
Taken contents of current file!
In TRY/CATCH - Out of Memory Exception - Try GC.Collect()
Memory used before collection: **498671500**
Memory used after collection: **250841460**
Try again with ReadFileString
How much content have we got? Content Length is **123595955**
We have content from the old log file

所以 GC.Collect 似乎解决了读取文件的问题。

但是我只是想知道它从这个调试中释放了什么样的内存,因为当我调用 GC.Collect() 时它正在删除 247.83MB 内存 .

因此我想知道它释放了什么样的对象或内存,因为我认为 .NET 应该具有良好的内置垃圾收集功能,如果它随着时间的推移产生这么多 "freeable" 内存应该我经常调用 GC.Collect() 来释放内存,或者只是因为它在第一次尝试将日志文件读入内存时失败而生成的内存量?

显然它已经有一段时间没有处理大文件了,直到我尝试了我以前从未使用过的 GC.Collect,所以我只是想知道内存从何而来,何时被收集通常,应该在其他地方调用它。

这是一个 windows 服务应用程序,它进入一个 DLL,该 DLL 在一天中使用 JSON 对第三方 API 进行多次 HTTP 调用,使用多个计时器来控制每个作业它需要 运行。因此,除非我手动停止服务,否则它会一直 运行。

所以我是否应该每晚调用一次 GC.Collect() 就像其他文章中人们所说的将垃圾收集留给系统一样好,但从这个实例来看,它有助于快速解决问题内存不足错误被抛出的地方(我有一台 14GB 64 位计算机,这是 运行ning on)。

垃圾收集器通常会自动收集不再使用和引用的托管对象。通常不需要(或不应该)手动调用 GC.Collect() 方法。 但是例如(在这种情况下)当你打电话时:

 queue.Dequeue(item)... 

在一个长循环中,没有指针或变量指向已删除的对象,但因为它仍在方法范围内,垃圾收集器不会收集它,直到内存变得非常低。遇到这种情况可以手动调用

首先非常仔细地检查您正在关闭(处置)所有文件对象,否则内部文件缓冲区将不会被释放,直到 GC 发现您忘记关闭文件。

如果不需要复制文件,只需重命名即可(FileInfo.Rename)。这是处理日志文件的正常方式。

如果您不需要处理数据,请使用 FileInfo.CopyTo or CopyTo(Stream) 方法,这就是为什么将使用合理的小缓冲区复制文本并且永远不需要分配内存来保存所有文本的原因同时

如果您确实需要处理文本,请一次读取一行,这将导致创建许多小字符串,而不是一个非常大的字符串。 .net GC 非常擅长回收小的短暂对象。 如果您同时将整个日志文件放在内存中,那是没有意义的。创建一个 returns 文件中的行的自定义迭代器是一种方法。

OutOfMemoryException 的 MSDN 页面是 OutOfMemoryException:

的主要原因

The common language runtime cannot allocate enough contiguous memory to successfully perform an operation. This exception can be thrown by any property assignment or method call that requires a memory allocation. For more information on the cause of the OutOfMemoryException exception, see "Out of Memory" Does Not Refer to Physical Memory.

This type of OutOfMemoryException exception represents a catastrophic failure. If you choose to handle the exception, you should include a catch block that calls the Environment.FailFast method to terminate your app and add an entry to the system event log...

关键是它代表灾难性故障,你应该退出你的应用程序。

调用 GC.Collect() 不再是一个选项 一旦发生此类异常。

调用 GC.collect 的合理解决方法是在关键代码部分之前创建一个新的 MemoryFailPoint

这当然没有解决真正的问题,为什么你的GC没有自己收集内存。

在您的情况下,您知道需要多少内存(文件大小),因此通过创建一个具有该大小的新 MemoryFailPoint,您可以合理地确定内存可用。 MemoryFailPoint 实际上会调用 GC.Collect 本身,如果它认为有必要的话,但它还有一些额外的逻辑来处理其他问题,例如页面文件大小或地址 space 碎片。

如果内存不足,您可以避免 OutOfMemoryException 及其潜在的破坏性副作用,取而代之的是 InsufficientMemoryException,可以毫无顾虑地捕获它。

我们真的只能猜测。由于您有足够的空闲内存(和非连续的虚拟地址 space),问题很可能与无法分配足够的连续内存有关。需要最连续内存的东西几乎都是数组,比如队列的后备数组。当一切正常时,地址 space 会定期压缩(GC 的一部分)并且您可以最大化可用的连续内存。如果这不起作用,则某些东西会阻止压缩正常工作 - 例如,固定句柄,如用于 I/O.

的句柄

为什么明确的 GC.Collect() 有帮助?很可能您正处于释放所有这些固定句柄的位置,并且压实确实有效。尝试使用 VMMap 或 CLRProfiler 之类的东西来查看对象在地址 space 中的布局方式 - 压缩问题的典型情况是内存中有 99% 的可用空间 space,但无处可去足以分配一个新对象(字符串和数组不能很好地处理内存碎片)。另一种典型情况是当您在分配非托管内存(例如缓冲区)时忽略使用 GC.AddMemoryPressure,因此 GC 不知道它应该真正开始收集。同样,CLRProfiler 在观察 GC 何时发生以及它如何映射到内存使用方面非常有帮助。

如果内存碎片确实是问题所在,您需要找出原因。这实际上有点复杂,并且可能需要使用 WinDbg 之类的东西,至少可以说,它很难使用。 I/O always 表示一些固定缓冲区,因此如果您并行执行大量 I/O,就会干扰 GC 的正常运行。 GC 试图通过创建多个堆来解决这个问题(取决于你 运行 的 GC 的确切配置,但看看你的情况,服务器 GC 应该真的是你正在使用的 - 你是 运行 这在 Windows 服务器上,对吧?),而且我已经看到为 "fix" 碎片问题创建了数百个堆 - 但最终,这注定要失败。

如果您必须使用固定句柄,您确实希望将它们分配一次,并在可能的情况下重新使用它们。固定阻止 GC 完成其工作,因此您应该只固定不需要在内存中移动的东西(大对象堆对象,堆底部的预分配缓冲区......),或者至少固定时间尽可能短。

一般来说,重用缓冲区是个好主意。遗憾的是,这意味着您要避免在这样的代码中使用 strings 和类似结构 - strings 不可变意味着您读取的每一行都需要是单独分配的对象。幸运的是,在你的情况下你不一定需要处理 strings - 一个简单的 byte[] 缓冲区也可以工作 - 只需寻找 0x13, 0x10 而不是 "\r\n" .您遇到的主要问题是您需要同时在内存中保存大量数据——您要么需要将其最小化,要么确保将缓冲区分配到最适合使用的地方;对于文件数据,LOH 缓冲区会有很大帮助。

避免如此多分配的一种方法是解析文件以查找行尾并仅记住要开始复制的行的偏移​​量。逐行(使用可重复使用的 byte[] 缓冲区),您只需更新 "at most 100 000th line from the end" 的偏移量,而不是分配和释放字符串。当然,这确实意味着您必须两次读取某些数据——这只是处理非固定长度 and/or 索引数据的代价 :)

另一种方法是从末尾读取文件。很难预测它的效果如何,因为它在很大程度上取决于 OS 和文件系统处理向后读取的能力。在某些情况下,它与前向阅读一样好——两者都是顺序阅读,只是关于 OS/FS 是否足够聪明来解决这个问题。在某些情况下,它会非常昂贵 - 如果是这种情况,请使用大文件缓冲区(例如 16 MiB 而不是更常见的 4 kiB 等)来尽可能地压缩顺序读取。从后面开始计数仍然不能完全允许您将数据直接流式传输到另一个文件(您需要将其与第一种方法结合使用,或者再次将整个 100 000 行再次保存在内存中),但这意味着你只读过你将要使用的数据(你读得最多的是你的缓冲区的大小)。

最后,如果所有其他方法都失败了,您可以将非托管内存用于您正在做的一些工作。我希望我不必说这比使用托管内存更棘手 - 您必须非常小心正确的寻址和边界检查等。对于像您这样的任务,它仍然很容易管理 - 最终,您只是用很少的 "work" 移动大量字节。不过,您最好充分了解不受管理的世界 - 否则它只会导致很难跟踪和修复的错误。

编辑:

既然您明确表示 "last 100k items" 是一种变通方法,而不是理想的解决方案,那么最简单的方法就是简单地流式传输数据,而不是将所有内容读取到 RAM 并一次性写入所有内容。如果 File.Copy/File.Move 对你来说不够好,你可以使用这样的东西:

var buffer = new byte[4096];
using (var sourceFile = File.OpenRead(...))
using (var targetFile = File.Create(...))
{
  var bytesRead = sourceFile.Read(buffer, 0, buffer.Length);
  if (bytesRead == 0) break;

  targetFile.Write(buffer, 0, bytesRead);
}

您唯一需要的内存是(相对较小的)缓冲区。