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

时间:2012-01-05 13:00:13

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

我编写了一个c#win表单应用程序,允许用户打开日志(文本)文件并查看数据网格中的日志行。应用程序格式化日志数据,以便用户可以过滤,搜索等。

我遇到的问题是当用户打开日志文件时>应用程序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收集,此行也不会影响内存使用量。

阅读了LOH之后,我假设这是因为当大型数组的部分被清空时,堆没有被压缩,所以尽管它的内容很多,它总是使用相同的1GB内存。

有什么方法可以减少解析数据时保留的内存量,或者可能更好地使用内存的返工?对我来说,一个300mb的文本文件放入字符串数组时会消耗1GB的内存,这似乎很奇怪?

感谢。

5 个答案:

答案 0 :(得分:3)

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

如果你将它与日志文件中的行一次一个地迭代(例如通过自己创建一个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方法,而不是创建自己的方法。

答案 1 :(得分:1)

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

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

答案 2 :(得分:1)

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

你的Array.Clear行可能没有帮助,因为logSection字符串不会被垃圾收集,事实上当循环迭代时,运行时间会很困难,因为找不到它是很困难的示例堆上的10K空间比找到10个1K空间。

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

答案 3 :(得分:0)

我首先要看到的是,您通过使用以下语句重复使用并将内存使用量增加一倍:

File.ReadAllLines(logPath).ToList();

系统将首先读入所有行,然后将其转换为使用量加倍的List。

我建议你使用以下方法通过streamreader读取文件:

  

使用(var sr = new StreamReader(fileName)){// Get Data out here}

这样一旦你离开陈述,记忆就会被处理掉。

此外,Array.Copy将使用更多内存,因此尝试在Using语句中创建和创建Desired对象,或者使对象IDIposposable,这样GarbageCollector可以节省一天。

答案 4 :(得分:0)

我建议不要将所有文件加载到内存中并使用延迟读取。对于&gt; = .NET 4,您可以利用File.ReadLines() Method来读取文件。

  

使用ReadLines时,可以开始枚举集合   返回整个集合之前的字符串;所以,当你   正在使用非常大的文件,ReadLines可以更有效。

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