大型字符串数组导致内存不足异常(C#)

3
我编写了一个C# Win Forms应用程序,允许用户打开日志(文本)文件并在数据网格中查看日志行。该应用程序格式化日志数据,以便用户可以进行过滤、搜索等操作。
我的问题是,当用户打开大于300MB的日志文件时,应用程序会抛出内存不足异常。
该应用程序首先将所有日志行加载到一个字符串数组中,然后循环遍历日志行,将日志条目对象添加到列表中。
var allLogLines = File.ReadAllLines(logPath).ToList();
var nonNullLogLines = allLogLines.Where(l => !string.IsNullOrEmpty(l));

this.ParseLogEntries(nonNullLogLines.ToArray());

这个初始步骤(将日志数据加载到字符串数组中)在任务管理器中使用了约1GB的内存。

internal override void ParseLogEntries(string[] logLines)
{
    this.LogEntries = new List<LogEntry>();
    this.LogLinesCount = logLines.Count();

    for (int i = 0; i < this.LogLinesCount; i++)
    {
        int entryStart = this.FindMessageCompartment(logLines, i);
        int entryEnd = this.FindMessageCompartment(logLines, entryStart + 1);
        int entryLength = (entryEnd - entryStart) + 1;

        if (entryStart + entryLength > this.LogLinesCount)
        {
            entryLength = this.LogLinesCount - entryStart;
        }

        var logSection = new string[entryLength];

        Array.Copy(logLines, entryStart, logSection, 0, entryLength);
        Array.Clear(logLines, i, entryLength - 1);

        this.AddLogEntry(logSection);

        i = (entryEnd - 1);
    }
}

AddLogEntry方法向列表(LogEntries)添加日志条目。在for循环中,成功解析了约50%的日志文件,然后发生了内存不足异常。此时任务管理器报告该应用程序使用了约1.3GB的内存。
如上所述,我已经添加了Array.Clear以清空已成功解析的日志数据部分,因此我期望随着对象被添加到集合中,大型日志数据数组使用的内存(一开始为1GB)会稳步减少,但事实并非如此。事实上,即使我定期添加GC Collect,这行代码也对内存使用量没有任何影响。
阅读了关于LOH的相关文章后,我认为这是因为堆没有在将大数组的某些部分置为空时进行压缩,因此它始终使用相同的1GB内存,尽管其内容不同。
是否有任何方法可以减少在解析数据时所占用的内存量,或者可能进行重新设计以更好地利用内存?对我来说,一个300MB的文本文件,当放入一个字符串数组中,消耗了1GB的内存,这似乎很奇怪?
谢谢。

2
什么是 FindMessageCompartment?同时不要使用数组,使用通用的 List<string> - sll
读取文件时使用ReadLine逐行读取并处理,有什么问题吗?而不是一次性加载整个文件。 - Zenwalker
这是在显示数据之前发生的吗,还是仅在解析过程中发生的? - user572559
你是如何读取文件的?你使用的是StreamReader.ReadLine()吗? - user572559
1
哪个版本的.NET?.NET 4提供了高效的方法来枚举文件行,而不需要获取所有行到内存中。 - sll
它是 .net 3.5。该文件包含分隔符字符串,用于表示日志部分的结束,FindMessageCompartment 函数查找分隔符的索引。这发生在显示任何数据之前,当用户单击“加载文件”时。 - RobJohnson
5个回答

4

你可以使用ParseLogEntry(string logLine)方法来解析单个日志行,而不是使用ParseLogEntries(string[] logLines)方法一次解析所有日志行。

如果结合逐行迭代日志文件(例如通过创建enumerator),这将避免在第一次创建大型数组string[] logLines

一种方法可能是这样的:

static IEnumerable<string> ReadLines(string filename)
{
    using (TextReader reader = File.OpenText(filename))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

// And use the function somewhere to parse the log

var logEntries = new List<LogEntry>()
foreach (string line in ReadLines("log.txt"))
{
    logEntries.Add(ParseLogEntry(line));
}

如果您使用的是.NET 4.0或更高版本,您当然可以使用sll在另一个答案中指出的File.ReadLines方法,而不是创建自己的方法。

只是提一下:ReadLines 方法是我从伟大的 Jon Skeet 的伟大书籍《C#深入》中学到的。;-) - Julian
我正在使用 .Net 3.5,Enumerator 看起来是一个理想的解决方案,非常感谢。 - RobJohnson

1

我知道这不是你问题的答案,但你可能想考虑不要完全将文件加载到内存中。

在你的情况下,你的日志文件需要300MB的内存,但如果需要2.5GB呢? 特别是如果结果是在数据网格中显示,你可能想使用分页,每次只加载一小块数据。


1

字符串在堆上需要连续的内存段;当堆上有很多长字符串并且尝试分配另一个字符串但没有可用的所需长度的段时,应用程序可能会抛出“内存不足”的异常。

你的Array.Clear行可能无法帮助,因为logSection字符串不会被垃圾回收,实际上随着循环迭代,运行时会变得困难,因为在堆上找到一个例如10K大小的空间比找到10个1K大小的空间更难。

这就是你的问题所在。至于解决方案,一般来说,我建议采用更懒惰的解决方案。你真的需要将所有这些字符串保存在主内存中吗?如果是的话,为什么不至少从StreamReader读取而不是加载到string[] logLines中?


0

首先我看到的是,您正在重复使用并且通过使用以下语句使内存使用量加倍:

File.ReadAllLines(logPath).ToList();

系统将首先读取所有行,然后将其转换为使用量加倍的列表。
我建议您通过流阅读器读取文件,使用以下代码: using(var sr = new StreamReader(fileName)) { // Get Data out here }
这样一来,一旦您离开语句,内存就会被处理掉。
此外,Array.Copy 将使用更多的内存,因此请尝试在 Using 语句中创建和创建所需的对象,或使您的对象可处置,以便垃圾回收器可以拯救一天。

1
它会使引用的内存使用量增加一倍,但与实际字符串数据相比(重新分配),这可能并不多。 - Joey

0

我建议不要将所有文件加载到内存中,而是使用惰性读取。对于 >= .NET 4,您可以利用 File.ReadLines() 方法 来读取文件。

当您使用 ReadLines 时,您可以在整个集合返回之前开始枚举字符串集合;因此,在处理非常大的文件时,ReadLines 可能更有效。

foreach (string line in File.ReadLines(@"path-to-a-file"))
{
   // single line processing logic
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接