【问题标题】:Should GC.Collect() be called regularly? [closed]应该定期调用 GC.Collect() 吗? [关闭]
【发布时间】:2016-04-11 09:33:47
【问题描述】:

我最近发布了一篇关于由于内存不足错误导致日志文件读取器失败的文章 > Out of memory error archiving a log file

在我有机会尝试更简单的方法(将日志文件命名为带有日期的名称以防止存档)之前,这显然意味着重写方法等,我首先尝试了垃圾收集选项,因为我从来没有使用它,例如 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 应该具有良好的内置垃圾收集功能,如果随着时间的推移它会产生这么多“可释放”的内存,我应该定期调用GC.Collect() 每隔一段时间就释放内存或仅仅因为它在第一次尝试将日志文件读入内存时失败而产生的内存量?

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

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

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

【问题讨论】:

  • 它没有解决你的问题,只是推迟了它。就像您可以尝试将项目移植到 x64 一样,这也会延迟问题。您应该避免一次将大文件加载到内存中,这是我想的真正问题。系统无法分配足够的连续内存。
  • 在大多数情况下,调用 GC.Collect 会让你更晚受苦,因为 GC 会将所有幸存的对象移至下一代,因此它们的寿命会更长!
  • 您可能有一堆小对象不足以强制进行 gen#2 集合。其中引用了大对象堆中的一个大块。 GC.Collect() 可以释放四分之一 jiggabyte 的那种场景。如果 GC 的行为不“自然”,那么必须提供帮助并没有错。请记住,永远不需要使用 LOH 来归档日志文件,当您一次复制一行而不是使用 File.ReadAllText() 之类的东西时,它也同样有效。减去这种故障模式。
  • 您的内存问题只是您的实际问题的症状:将整个文件读入内存。对于您要解决的特定问题,只需考虑重命名(使用 File.Move)文件(或复制使用 File.Copy 的文件)。这样您就可以完全避免内存问题。

标签: c# .net io garbage-collection readfile


【解决方案1】:

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

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

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

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

【讨论】:

  • 请问为什么 File.ReadAllText 方法在垃圾收集器之后起作用。那么该方法如何工作。在我输入 GC.Collect() 之前,我的 using 语句和 File.ReadAllText 方法现在都失败了,当第一个抛出内存不足错误时,我调用 GC.Collect() 然后 ReadAllText() 并且它工作正常。取出 GC.Collect() 并且 ReadAllText 因内存不足而失败。那么 ReadAllText 是如何工作的。移动不只是像其他语言那样在场景下结合复制和删除吗?有人说要重命名文件,然后移动。所以 CopyTo 是最好的吗?
  • @MonkeyMagix 当文件存储在磁盘上时,它不会存储在“文件夹”中,该文件夹仅包含文件名列表和每个文件存储的地址。因此,一个文件可以被重命名和/或移动到不同的文件夹,不接触文件中的任何数据。
  • 所以大部分动作不需要复制任何数据。因此,如果您没有理由“处理”日志文件中的数据,请进行重命名或移动(使用 FileInfo 类)。
  • @MonkeyMagix,为什么 GC 使您的软件“工作”也是如此,请参阅 Han 对您的问题的评论,或者您可能没有处理您应该处理的对象。
  • @IanRingrose - 值得注意的是,GC 从不直接处理对象。 GC可能调用一个终结器,但是很难知道哪些对象有终结器调用.Dispose()。我们永远不应该依赖 GC 来影响 .Dispose() 调用。
【解决方案2】:

我们真的只能猜测。由于您有足够的可用内存(和不连续的虚拟地址空间),因此问题很可能与无法分配足够的连续内存有关。需要最连续内存的东西几乎是排他性的数组,比如队列的后备数组。当一切正常时,地址空间会定期压缩(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 完成其工作,因此您应该只固定不需要在内存中移动的东西(大型对象堆对象、堆底部的预分配缓冲区......),或者至少引脚的时间越短越好。

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

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

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

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

编辑:

由于您明确表示“最后 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);
}

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

【讨论】:

  • 正如我在之前的评论中所说,我已经重写了它以使用 FileInfo CopyTo 方法,如果我今晚写得不好的话,它会退回到 100k。如果它有效,我将删除该后备。还有一些人一直说队列导致了内存错误,它不是,这是有效的。抛出它的是 using(streamreader) OR File.ReadAllText 方法。在流读取器抛出它的那一刻,我调用 GC.Collect 然后 File.ReadAllText 工作。在两者都因内存不足错误而失败之前
  • 从来没有想过什么时候 io 对象被固定,从而显式调用 GC.Collect 是值得的。但是我 99% 确信 OP 可以解决他/她的问题,而无需在 ram 中存储大量数据,因此完全避免了 GC 问题。
  • @MonkeyMagix 当然是File.ReadAllText - 这就是需要这么多连续内存的原因(见我的回答)。但这并不意味着这是真正的原因——真正的原因是(很可能)碎片化的地址空间。否则,GC 会简单地通过压缩堆并进行收集来确保有 足够的内存。事实上,您刚刚创建了最糟糕的情况 - 您在请求内存中的巨大字符串之前分配了固定缓冲区就在堆的顶部。堆永远不能被压缩以腾出足够的空间,即使是部分空间。
  • File.ReadAllText 是 try/catch 中的第二次尝试。如果您阅读这篇文章,第一次尝试是我的 using(streamreader) 函数,该函数会引发内存不足错误,我想这会做同样的事情,因为它会一次性消耗整个文件(即使上面有人使用 ReadAllText 读取12GB 没有错误?) >>> using (StreamReader reader = new StreamReader(path)) { return reader.ReadToEnd(); }
  • 正如我在另一条评论中所说,当我调用 GC.Collect() 在第一次调用该 using(Streamreader) 方法之后 - File.ReadAllText 在清除 20GB 内存后完美读取文件。我在 16GB 64 位 PC 上运行它,有 8GB 可用空间。文件大小小于 1GB。我今晚正在尝试复制/移动方法来查看,但我不明白为什么当您复制大文件时它们不会像操作系统那样做,例如在执行数据传输时弹出窗口X 秒/分钟。
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

    来自 OutOfMemoryException 的 MSDN 页面,这是导致 OutOfMemoryException 的主要原因:

    公共语言运行时无法分配足够的连续内存来成功执行操作。任何需要内存分配的属性分配或方法调用都可能引发此异常。有关 OutOfMemoryException 异常原因的详细信息,请参阅“内存不足”不引用物理内存。

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

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

    一旦发生此类异常,调用GC.Collect()不再是一种选择

    【讨论】:

    • 我知道它这么说,但它似乎清除了 20+ MB 的内存,然后允许 ReadAllText() 方法在以前没有读取长日志文件时读取。所以我将它困在 System.Outofmemory 异常中并在那里运行它。但无论如何,我现在已经改变了流程。
    【解决方案5】:

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

     queue.Dequeue(item)... 
    

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

    【讨论】:

    • 范围界定没有相关性。无论范围如何,只要将来没有读取本地信息,就可以进行收集。您必须编写非常复杂的代码以防止其正常工作。范围只对编译器和调试器很重要——在调试器中运行时,.NET 将尊重本地范围以帮助调试。毕竟,如果GC.Collect 可以收集到这些对象,那么它们肯定已经无法访问了。
    • 完全是垃圾! Luann 是正确的。
    • 顺便说一下 queue.Dequeue 方法不是问题。这是我当前的备份解决方案,当前两个方法 File.ReadAllText() 和自定义 ReadFileString(使用 using() 语句和流读取器)都因内存不足错误而失败时,它可以让我获得最后 100,000 行。所以这两种方法都失败了,然后我使用队列方法来获取一些日志文件。现在,当我在流读取器方法失败后调用 GC.Collect() 时,File.ReadAllText() 可以工作。那么为什么 File.ReadAllText() 与使用中的流阅读器相比没有掉下来呢?
    • @IanRingrose 如果 op 在 Debug 中运行会怎样?
    • @Mafii,我可以看到它对他的代码有何影响。此外,在调试中运行时,即使强制 GC 也不会回收对象,直到它对调试器不可见。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-11
    • 1970-01-01
    • 2013-08-05
    • 2011-09-01
    • 2023-01-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多