使用StreamReader时出现了内存泄漏问题。

6
我有几个非常大的文件,每个文件大小为500MB++,包含整数值(实际上更复杂一些)。我正在循环读取这些文件,并计算所有文件的最大值。但由于某种原因,在处理过程中内存不断增长,似乎GC永远不释放由先前的lines实例获取的内存。

我无法流式传输数据,必须针对每个文件使用GetFileLines。如果为一个文件存储lines所需的实际内存量为500MB,那么在处理10个文件后,为什么会使用5GBRAM?最终在处理15个文件后崩溃并出现“内存不足”异常。

计算:

   int max = int.MinValue;

   for (int i = 0; i < 10; i++)
   {
      IEnumerable<string> lines = Db.GetFileLines(i);

      max = Math.Max(max, lines.Max(t=>int.Parse(t)));
   }

GetFileLines 代码:

   public static List<string> GetFileLines(int i)
   {
      string path = GetPath(i);

      //
      List<string> lines = new List<string>();
      string line;

      using (StreamReader reader = File.OpenText(path))
      {
         while ((line = reader.ReadLine()) != null)
         {
            lines.Add(line);
         }

         reader.Close();
         reader.Dispose(); // should I bother?
      }

      return lines;
   }

你调用了 lines.Clear() 方法吗? - opewix
10 x 500 Mb = 5 GB。您将所有文件内容保存在列表List<string> lines中。与ReadAllLines()相同的性能。 - Cybermaxs
这并不是这样的,因为每个文件都有一个新的列表实例,垃圾回收器应该清理上一个实例。 - Murtuza Kabul
用作进一步处理的数据源。 - user1514042
@user1514042,我已经为您提供了一种解决方案,允许您一次性读取整个文件,执行所有处理,然后删除存储行所使用的内存。 - Mike Perrenoud
显示剩余2条评论
6个回答

6

对于非常大的文件,ReadLines 方法是最合适的选择,因为它具有延迟执行的特性,不会将所有行加载到内存中,并且使用简单:

  Math.Max(max, File.ReadLines(path).Max(line => int.Parse(line)));

更多信息:

http://msdn.microsoft.com/zh-cn/library/dd383503.aspx

编辑:

ReadLines 的实现原理如下:

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

此外,建议在有多个文件时使用并行处理以提高性能。

抱歉,处理逻辑更加复杂,示例仅表明它与读取过程完全解耦。 - user1514042
@user1514042:这并不重要,你可以使用它与LINQ一起处理非常大的文件。 - cuongle
当实际行数达到500K ++时,这是否会影响性能? - user1514042
@user1514042:是的,仍然循环查找最大值,但不会将所有内容加载到内存中以避免泄漏。 - cuongle
@user1514042:我的意思是使用ReadLines而不是ReadAllLines,你没有抓住重点。 - cuongle
显示剩余7条评论

4
您可能因为在处理完数据后仍然在内存中保留解析结果的引用而导致崩溃(您展示的代码并没有这样做,但是您运行的是否是同样的代码?)。很难想象在StreamReader中有这样的错误。
您确定必须一次性读取整个文件吗?使用可枚举的行序列作为IEnumerable<string>可能是完全可行的,而不是预先加载一个List<string>。至少在这段代码中,没有任何禁止这样做的内容。
最后,CloseDispose调用是多余的;using语句会自动处理它们。

我只使用值类型,它们仍然能够保存引用吗? - user1514042
当然可以。如果你以某种方式访问了列表,那么肯定有人持有对它的引用。 - Jon
是的,但它每次都被替换,如果我不满意最后500MB没有被清除,你的观点就是正确的,但我有一个不同的问题。 - user1514042
1
@user1514042:如果你的内存不足,那么肯定有一些引用没有被清除掉。就是这么简单。 - Jon
@user1514042,朋友,请注意你的言辞。你肯定没有按照自己想象中的方式管理内存,否则你就不会出现内存不足的情况了。请记住,这行代码 IEnumerable<string> lines = Db.GetFileLines(i); 每次都会复制列表,但仅仅替换前一个引用,因此前一个 List<string> 仍然存在于堆上。 - Mike Perrenoud
@user1514042:非常正确。也许你是想和Mike交流吧? - Jon

1
为什么不按照以下方式实施:
int max = Int32.MinValue;
using(var reader = File.OpenText(path)) 
{
    while ((line = reader.ReadLine()) != null)
    {
         int current;
         if (Int32.TryParse(line, out current))
             max = Math.Max(max, current);
     }    
}

0

好的,如果你想要一个可以一次性读取整个文件的解决方案,因为你确信这样可以提高性能,那么我们可以这样做,这样就不会出现内存问题。

public static int GetMaxForFile(int i) 
{ 
    string path = GetPath(i); 

    var lines = new List<string>(File.ReadAllLines(path));

    // you MUST perform all of your processing here ... you have to let go
    // of the List<string> variable ...
    int max = Math.Max(max, lines.Max(t=>int.Parse(t)));

    // this may be redundant, but it will cause GC to clean up immediately
    lines.Clear();
    lines = null;

    return max;
} 

0

你正在将整个文件读入内存(List lines)

我想你可以一次只读取一行并保留最高的数字?

这将节省大量的RAM。


每行需要0.5秒来处理,这就是为什么读取它们然后进行处理会更快。我们通过性能测试证实了这一点,因此我们获得了很多好处。 - user1514042

0

看起来你总是在内存中加载整个文件。同时,你还为文件的每一行创建托管对象(List)。

没有理由让你的内存使用量增加。

请发布代码的其余部分,我怀疑你可能在某个地方引用了正在使用的列表,因此它没有被处理。


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