.NET堆填充了字符串对象 - > OutOfMemoryException异常

时间:2012-02-19 11:10:16

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

我经常(每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字节)。

在Memory Profiler中,当我跟踪什么是使用字符串对象时,我可以看到它主要是(93%)使用字符串对象的XElement对象。

服务器有6个cpu和6GB的RAM,所以我看不出为什么我得到了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();
    }

6 个答案:

答案 0 :(得分:2)

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

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

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

的变量'dir'没有做任何事情。每个方法执行创建两次大型数组。

答案 1 :(得分:1)

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

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

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

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

此外,我不认为你的StringBuilders在这种情况下有所帮助。 StringBuilders在构建多部分字符串时非常有用,并且不希望在编写它们时创建多个中间字符串。看来这里所有的字符串连接总是只使用两个字符串 - 基本名称和新后缀 - 所以我不确定它实际上是在节省你的时间或内存。

答案 2 :(得分: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,您可以减少创建的ManualResetEvent的数量(及其已忘记的 Dispose()

Parallel.ForEach(GetAllFiles(RawDataDirectory.FullName) , file =>
{
    //ImportFileJob newJob = new ImportFileJob(file);
    //newJob.Run
    Console.WriteLine(file);
}); 
顺便说一下,您还应该看到CountdownEvent

答案 3 :(得分:1)

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

通过这种方式,您的程序会收到更改的确切文件的通知,您甚至不必为您不关心的文件阵列分配内存。

答案 4 :(得分:0)

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

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

不确定每个导入的工作量是多少,但是您是否为每个目录而不是每个文件尝试了一个线程?

答案 5 :(得分:0)

正如其他人所说,

1)减少字符串操作。

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

2)你的专栏“var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);” 似乎是多余的。看起来你没有使用它。因此,删除此代码,它持有大量的字符串引用。

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

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.
}

希望这会有所帮助。