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

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

在我有机会尝试更简单的方法(调用日志文件的名称,其中有一个日期以防止存档)这显然意味着重写方法等,我首先尝试垃圾收集选项,因为我从来没有使用它,例如GC.Collect的()。

如果在尝试读取日志文件内容时抛出内存错误,则将其置于第一次try / catch中,并且它似乎释放了一半内存,例如从此过程中使用的调试文件中释放(因为日志文件明显不在动作所以这是为了帮助我调试事后)我从昨晚的归档过程得到这个回应。

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应该具有良好的内置垃圾收集function,并且如果它随着时间的推移制作这么多“可用的”内存,我应该定期调用GC.Collect ()经常释放内存或产生的内存量只是因为它在第一次尝试将日志文件读入内存时失败了?

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

这是一个Windows服务应用程序,它使用多个计时器来控制运行所需的每个作业,从而使用JSON对第三方API进行许多HTTP调用。 因此,除非我手动停止服务,否则它会持续运行。

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

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

  queue.Dequeue(item)... 

在长循环中,没有指向已删除对象的指针或变量,但由于它仍在方法范围内,因此垃圾收集器在内存变得非常低之前不会收集它。 如果你处于这样的危急情况,你可以手动调用它。

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

如果您不需要复制文件,只需重命名它(FileInfo.Rename)。 这是处理日志文件的常用方法。

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

如果确实需要处理文本,一次读取一行,这将导致创建大量小字符串,而不是一个非常大的字符串。 .net GC非常擅长回收小型短寿命物体。 如果您将整个日志文件同时存储在内存中,那就没有意义了。 创建一个返回文件中的行的自定义迭代器将是一种方法。

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

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

如果内存碎片确实是问题,你需要找出原因。 这实际上有点复杂,并且可能需要一些像WinDbg这样的工作,至少可以说它很难使用。 I / O 总是意味着一些固定缓冲区,所以如果您并行执行大量I / O,则会干扰GC的正常运行。 GC尝试通过创建多个堆来处理这个问题(取决于您正在运行的GC的确切配置,但是看一下您的情况,服务器GC应该是您正在使用的 – 您在Windows Server上运行它,对吧?),我已经看到数百个堆被创建来“修复”碎片问题 – 但最终,这注定要失败。

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

通常,重用缓冲区是个好主意。 遗憾的是,这意味着你要避免像这样的代码中的string和类似的结构 – string s是不可变的意味着你读取的每一行都需要是一个单独分配的对象。 幸运的是,在你的情况下你不一定需要处理string s – 一个简单的byte[]缓冲区也可以正常工作 – 只需要查找0x13, 0x10而不是"\r\n" 。 您遇到的主要问题是您需要同时在内存中保存大量数据 – 您需要将其最小化,或者确保将缓冲区分配到最佳使用位置; 对于文件数据,LOH缓冲区将有很大帮助。

避免这么多分配的一种方法是解析文件,寻找行尾并记住要开始复制的行的偏移​​量。 当你逐行(使用可重复使用的byte[]缓冲区)时,你只需更新“从最后到第100万行”的偏移量,而不是分配和释放字符串。 当然,这确实意味着你必须两次读取一些数据 – 这只是处理非固定长度和/或索引数据的价格:)

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

最后,如果所有其他方法都失败了,您可以使用非托管内存来完成您正在进行的一些工作。 我希望我不必说这比使用托管内存要复杂得多 – 除了其他方面,你必须非常小心正确的寻址和边界检查。 对于像你这样的任务,它仍然是可管理的 – 最终,你只需要很少的“工作”来移动大量的字节。 不过你更好地理解了非托管世界 – 否则它只会导致很难跟踪和修复的漏洞。

编辑:

既然你明确表示“最后的100k项目”是一种解决方法而不是理想的解决方案,最简单的方法就是简单地传输数据,而不是将所有内容读入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); } 

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

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

这当然不能解决真正的问题,为什么你的情况下GC不会自己收集内存。

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

如果内存不足,则可以避免出现具有潜在破坏性副作用的OutOfMemoryException ,而是获得InsufficientMemoryException ,可以毫无后顾之忧地捕获它。

从OutOfMemoryException的MSDN页面,这是OutOfMemoryException的主要原因:

公共语言运行库无法分配足够的连续内存来成功执行操作。 任何需要内存分配的属性赋值或方法调用都可以抛出此exception。 有关OutOfMemoryExceptionexception原因的详细信息,请参阅“内存不足”不引用物理内存。

这种类型的OutOfMemoryExceptionexception表示灾难性故障。 如果您选择处理exception,则应该包含一个调用Environment.FailFast方法的catch块来终止您的应用并向系统事件日志添加一个条目…

关键是它代表了一个灾难性的失败 ,你应该退出你的应用程序。

一旦发生此类exception, 就不再调用GC.Collect()