【问题标题】:.NET heaps filled up with string object -> OutOfMemoryException.NET 堆填满了字符串对象 -> OutOfMemoryException
【发布时间】:2012-03-10 01:22:08
【问题描述】:

我经常(每 30-60 分钟)在我的 Windows 服务中收到 System.OutOfMemoryException。该服务的工作是遍历包含数据文件的 6 个目录,服务会将这些数据文件清洗成通用的 XML 数据格式。

这 6 个文件夹每个包含 5-10.000 个文件,因此文件总数约为 45.000 个,并且在当天添加了新文件。每天增加大约 1-2000 个新文件。文件大小在 4KB 到 500KB 之间。

每个数据文件都通过XElement对象清洗成通用的XML数据格式。

我在服务上使用了 RedGates ANTS Memory Profiler,使用最多内存的对象是字符串(大约 90.000.000 字节)和 XElement(大约 51.000.000 字节)。

在内存分析器中,当我跟踪使用字符串对象的内容时,我可以看到大部分(93%)是 XElement 对象在使用字符串对象。

服务器有 6 个 CPU 和 6GB 内存,所以我不明白为什么我会收到 OutOfMemoryException。如果我查看进程中的 Windows 服务,它的最大 RAM 使用量为 1.2GB。

我读到.NET 垃圾收集器不会清除字符串对象,因为字符串对象存储在实习表中。这可能是错误,如果是我该怎么办?

下面的代码显示了我如何循环文件。如您所见,我也尝试一次获取 20 个文件。这只会将 OutOfMemoryException 推送几个小时,因此服务将运行 4-5 小时而不是 30-60 分钟。

为什么会出现 OutOfMemoryException?

private static void CheckExistingImportFiles(object sender, System.Timers.ElapsedEventArgs e)
    {
        CheckTimer.Stop();
        var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);

        List<ManualResetEvent> doneEvents = new List<ManualResetEvent>();
        int i = 0;
        //int doNumberOfFiles = 20;

        foreach (string existingFile in Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories))
        {
            if (existingFile.EndsWith("ignored") || existingFile.EndsWith("error") || existingFile.EndsWith("importing"))
            {
                //if (DateTime.UtcNow.Subtract(File.GetCreationTimeUtc(existingFile)).TotalDays > 5)
                //  File.Delete(existingFile);
                //continue;
            }

            StringBuilder fullFileName = new StringBuilder().Append(existingFile);

            if (!fullFileName.ToString().ToLower().EndsWith("error") && !fullFileName.ToString().ToLower().EndsWith("ignored") && !fullFileName.ToString().ToLower().EndsWith("importing"))
            {
                File.Move(fullFileName.ToString(), fullFileName + ".importing");
                fullFileName = fullFileName.Append(".importing");

                ImportFileJob newJob = new ImportFileJob(fullFileName.ToString());

                doneEvents.Add(new ManualResetEvent(false));

                ThreadPool.QueueUserWorkItem(newJob.Run, doneEvents.ElementAt(i));
                i++;
            }

            //if (i > doNumberOfFiles)
            //{
            //    i = 0;
            //    doNumberOfFiles = 20;
            //    break;
            //}
        }
        i = 0;
        WaitHandle.WaitAll(doneEvents.ToArray());

        CheckTimer.Start();
    }

【问题讨论】:

  • ImportFileJob 做什么?它是如何实现的?
  • 您对StringBuilder 的使用是多余的。 IOW 没有任何好处。
  • ImportFileJob 获取数据文件并使用 xlst 样式表将数据文件转换为通用 XML 数据文件。
  • 我在应用程序中使用了 List,导致服务在 3-5 分钟后出现内存不足异常。 ANTS 内存分析器告诉我,原因是 List。用 List 改变它解决了这个问题。但是异常不断出现,只是没有那么快,所以我尝试用 StringBuilder 替换所有字符串,因为我读到字符串对象不是由 .NET GC 收集的。
  • 能否也显示 ImportJob 的代码?此方法中没有 XDocument,所以我猜测任何导致 51MB 的 XDocument 价值的问题都隐藏在那里。

标签: c# .net out-of-memory heap-memory


【解决方案1】:

正如 Avner Shahar-Kashtan 已经说过的,我也认为问题出在ImportJob(你还没有向我们展示它的代码)。

即便如此,您仍然可以进行一些优化。

您不必一次加载所有文件名。它可以通过 dir 来完成,如下所示

IEnumerable<string> GetAllFiles(string dirName)
{
    var dirs = Directory.GetDirectories(dirName);

    foreach (var file in Directory.GetFiles(dirName))
        yield return file;

    foreach (var dir in dirs) //recurse
        foreach (var file in GetAllFiles(dir)) 
            yield return file;
}

通过使用 TPL,您可以减少创建的 ManualResetEvents 的数量(以及它们的忘记 Dispose()s)

Parallel.ForEach(GetAllFiles(RawDataDirectory.FullName) , file =>
{
    //ImportFileJob newJob = new ImportFileJob(file);
    //newJob.Run
    Console.WriteLine(file);
}); 

顺便说一句,您还应该看到CountdownEvent

【讨论】:

  • ImportFileJob 类做了很多不同的事情,并且 XElement 类被使用了很多,所以你很清楚问题可能在这里。 .NET 垃圾收集器不应该清理 XElement 对象吗?
  • 可能有数百万个原因。我不知道你的代码。但是没有理由怀疑 XElement 或垃圾收集器,除非您不释放资源(如文件)
【解决方案2】:

您可以使用 FileSystemWatcher,而不是使用计时器并循环遍历文件夹的所有内容:http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx

这样,您的程序就会收到更改的确切文件的通知,您甚至不必为您不关心的文件数组分配内存。

【讨论】:

    【解决方案3】:

    正如其他人建议的那样,

    1) 减少字符串操作。

    您的目录似乎返回“太多”文件名(字符串),因此需要注意。

    2) 你的线路'var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);' 似乎是多余的。看起来你没有使用它。所以,删除这段代码,它包含很多字符串引用。

    3) 如果可能,迭代从目录返回的文件块(比如 10K)。所以这需要编写一个将 List 拆分为 List> 的代码,然后在遍历外部循环时清除内部列表持有的引用。 有点像,

    foreach(List<List<string>> fileNamesInChunk in GetFilesInChunk(directoryName)){
         foreach(var fileName in fileNamesInChunk){
         //Do the processing.
         }
         fileNamesInChunk.Clear(); //This would reduce the working set as you proceed.
    }
    

    希望这会有所帮助。

    【讨论】:

      【解决方案4】:

      您在 If 语句中调用 fullFileName.ToString().ToLower() 三次。将此字符串值缓存在局部变量中并使用您的 if 语句(为您节省三个临时字符串)。

      尝试使用 XmlWriter 而不是 XDocument。 XDocument 是内存中的对象图,因此对于大型数据集,它可能不是最高性能的(您将整个事物保存在内存中,直到将其作为一个整体写入磁盘)。使用 XmlWriter,您通常可以逐个元素流式传输到文件缓冲区,内存占用的要求要低得多。

      不确定每次导入的工作量,但您是否尝试过每个目录而不是每个文件的线程?

      【讨论】:

        【解决方案5】:
        Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);
        

        这将返回一个数组。如果目录中的文件与您所说的一样多,那么这些将是非常大的数组,大到足以放置在大对象堆中。那里的多个海量数组很容易导致 OutOfMemoryException。以下行没有帮助

        var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);
        

        具有不做任何事情的变量“dir”。每次方法执行都会创建两次大数组。

        【讨论】:

          【解决方案6】:

          我可以立即发现一些简单的优化。

          您使用了很多fullFileName.ToString().ToLower().EndsWith("ignored") 电话。这些有很多开销,因为您总是使用给定的字符串并创建一个新的小写字符串。

          相反,您应该使用允许不区分大小写比较的 Endswith(或 Contains)重载:

          fullFileName.ToString()
            .EndsWith("ignored", StringComparison.CurrentCultureIgnoreCase)
          

          另外,我认为您的 StringBuilders 在这种情况下没有帮助。当您构建多部分字符串并且不希望在编写它们时创建多个中间字符串的开销时,StringBuilders 最有用。似乎您在这里的所有字符串连接始终只使用两个字符串 - 基本名称和新后缀 - 所以我不确定它是否真的为您节省了任何时间或内存。

          【讨论】:

          • -1。你是对的,但这些都不应该导致 hthat 问题 - 这些字符串会立即被丢弃。
          猜你喜欢
          • 1970-01-01
          • 2019-06-28
          • 1970-01-01
          • 2018-12-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-06-24
          • 2018-03-06
          相关资源
          最近更新 更多