数据表不释放内存

11

我有一个数据加载过程,将大量数据加载到DataTable中进行一些数据处理,但每次作业完成后,DataLoader.exe(32位,具有1.5G内存限制)并未释放所有已使用的内存。

我尝试了三种释放内存的方法:

  1. DataTable.Clear()然后调用DataTable.Dispose()(释放约800MB内存,但仍会在每次数据加载作业完成后增加约200MB内存,在3或4次数据加载后,由于总共超过了1.5G内存,将抛出内存不足异常)
  2. 将DataTable设置为null(没有释放内存,而且如果选择加载更多的数据,将抛出内存不足异常)
  3. 直接调用DataTable.Dispose()(没有释放内存,而且如果选择加载更多的数据,将抛出内存不足异常)

以下是我尝试的测试代码(在实际程序中,它不会递归调用,而是由某些目录监视逻辑触发。该代码仅用于测试。对于造成的混淆,我表示抱歉。):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;

namespace DataTable_Memory_test
{
class Program
{
    static void Main(string[] args)
    {
        try
        {
            LoadData();                
            Console.ReadKey();

        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            Console.ReadKey();
        }
    }

    private static void LoadData()
    {
        DataTable table = new DataTable();
        table.Columns.Add("Dosage", typeof(int));
        table.Columns.Add("Drug", typeof(string));
        table.Columns.Add("Patient", typeof(string));
        table.Columns.Add("Date", typeof(DateTime));

        // Fill the data table to make it take about 1 G memory.
        for (int i = 0; i < 1677700; i++)
        {
            table.Rows.Add(25, "Indocin", "David", DateTime.Now);
            table.Rows.Add(50, "Enebrel", "Sam", DateTime.Now);
            table.Rows.Add(10, "Hydralazine", "Christoff", DateTime.Now);
            table.Rows.Add(21, "Combivent", "Janet", DateTime.Now);
            table.Rows.Add(100, "Dilantin", "Melanie", DateTime.Now);
        }
        Console.WriteLine("Data table load finish: please check memory.");
        Console.WriteLine("Press 0 to clear and dispose datatable, press 1 to set datatable to null, press 2 to dispose datatable directly");
        string key = Console.ReadLine();
        if (key == "0")
        {
            table.Clear();
            table.Dispose();
            Console.WriteLine("Datatable disposed, data table row count is {0}", table.Rows.Count);
            GC.Collect();   
            long lMemoryMB = GC.GetTotalMemory(true/* true = Collect garbage before measuring */) / 1024 / 1024; // memory in megabytes
            Console.WriteLine(lMemoryMB);

        }
        else if (key == "1")
        {
            table = null;
            GC.Collect();
            long lMemoryMB = GC.GetTotalMemory(true/* true = Collect garbage before measuring */) / 1024 / 1024; // memory in megabytes
            Console.WriteLine(lMemoryMB);
        }
        else if (key == "2")
        {
            table.Dispose();
            GC.Collect();
            long lMemoryMB = GC.GetTotalMemory(true/* true = Collect garbage before measuring */) / 1024 / 1024; // memory in megabytes
            Console.WriteLine(lMemoryMB);
        }
        Console.WriteLine("Job finish, please check memory");
        Console.WriteLine("Press 0 to exit, press 1 to load more data and check if throw out of memory exception");
         key = Console.ReadLine();
        if (key == "0")
        {
            Environment.Exit(0);
        }
        else if (key == "1")
        {
            LoadData();
        }
    }
  }
}

1
DataTable 本身并没有实现 Dispose() 方法,它是从其父类 MarshalByValueComponent 中继承的。而 MarshalByValueComponent 只做了两件事情:如果你在代码示例中使用了 ISite 来包含数据表,则调用 Site.Container.Remove(this) 方法;并引发 Disposed 事件。它不会释放任何资源。 - Scott Chamberlain
@ScottChamberlain 好的,我会尝试在发布模式下运行。 - mhan0125
1
在没有调试器的情况下运行更为重要,以发布模式运行只会做出一些小的改变,但是在没有调试器的情况下运行则会对行为产生重大影响。 - Scott Chamberlain
@ScottChamberlain 我尝试直接运行exe文件而不使用调试器,但仍然抛出了OutOfMemory异常。 - mhan0125
让我们在聊天中继续这个讨论 - mhan0125
显示剩余5条评论
4个回答

7
您的主要问题是垃圾回收器的行为取决于是否处于调试模式或在没有调试器的发布模式下。在调试版本或带有调试器的发布版本中,所有对象的生命周期都延长到整个方法的生命周期。这意味着,在完成LoadData方法之前,table不能被GC回收。这就是为什么您不断耗尽内存的原因。
如果将程序更改为发布模式并在没有调试器的情况下运行,则一旦您通过代码路径中变量table指向的最后一个引用,该对象就变得符合垃圾回收条件,您就可以获得释放的内存。
垃圾回收器在“可调试情况”下更改其行为的原因是将调试器本身视为持有当前执行代码范围内所有变量的引用。如果没有这样做,您将无法查看观察窗口或鼠标悬停在变量上的值。因此,直到变量超出范围或覆盖变量,您才能“传递对对象的最后一个引用”。

请查看博客文章《垃圾回收、作用域和对象生命周期》,以获取有关该过程的更详细信息。


3

如果您将要求重复的部分移动到函数外部,内存将被正确释放(仅测试了方法1(Clear和Dispose)):

static void Main(string[] args)
{
    try
    {
        string key;
        do
        {
            LoadData();
            Console.WriteLine("Job finish, please check memory");
            Console.WriteLine("Press 0 to exit, press 1 to load more data and check if throw out of memory exception");
            key = Console.ReadLine();
        } while (key == "1");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
        Console.ReadKey();
    }
}

可能当对象超出作用域时,它们的内存会被释放


我认为codroipo的方法是正确的,它在每次运行后释放内存。 - mhan0125
1
@ScottChamberlain 通过保留对 DataTable 的引用,操作员防止 GC 收集任何剩余数据,例如更改跟踪数据。在 Release 模式下运行不会改变这一点。简单的解决方案是 codroipo 提出的 - 不要重用变量,将其置空或更好地,在另一个方法中移动它,以便在超出作用域时可以被收集。 - Panagiotis Kanavos
@PanagiotisKanavos 如果您在最后一次使用“table”之后没有使用调试器运行,则该对象可以进行垃圾回收。当您使用调试器附加到方法时,所有对象的生命周期都会延长到方法结束(由于他使用递归测试方法,变量将不会被释放直到他退出程序)。 - Scott Chamberlain
2
@PanagiotisKanavos,我不是在谈论发布模式或调试模式,我是在谈论当JITer与调试器附加和未附加时如何处理对象的寿命。请阅读《垃圾收集、作用域和对象寿命》(http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2005/02/17/5272.aspx),它详细解释了这种差异。 - Scott Chamberlain
1
@PanagiotisKanavos 不,你没有理解重点,如果你没有附加调试器并处于发布模式下,则“什么是活着的”和“什么不是活着的”的规则会发生变化。如果你在x86模式下以发布模式构建OP的程序,并在没有调试器的情况下运行它,我无法让它泄漏内存,每次GC.Collect()后它总是回到11 MB。在调试器中运行或在Debug模式下运行会使它像OP看到的那样崩溃。 - Scott Chamberlain
显示剩余6条评论

2

1

没有一种方式可以像没有内存管理的代码那样强制C#释放内存。了解.NET垃圾收集器的工作原理会有所帮助。基本上,.NET应用程序中的内存使用量会上升到三种情况之一,这将触发垃圾收集。我在以下问题的答案中描述了这个过程:

清理方法中的变量

避免OutOfMemory异常的一种方法是利用MemoryFailPoint类,它允许您设置一个故障点,超出该点将抛出InsufficientMemoryException,从而为您提供减缓进程直到另一个工作线程可用的机会。我不确定这是否是您想尝试的,但这是可用的:

https://msdn.microsoft.com/en-us/library/system.runtime.memoryfailpoint%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396


1
一个更简单的解决方法是将变量设置为null并创建一个新实例。通过保持相同的对象存在,内部结构永远不会被释放。 - Panagiotis Kanavos
用户在执行此操作时看到了“OutOfMemory”错误。在狭窄的范围内使用对象是一个好主意,但仅仅将其设置为“null”并不足以触发垃圾回收,正如我上面所指出的那样。 - maniak1982
1
没有说它会这样做。但是不将其设置为null可以防止垃圾回收。无论如何,我刚刚注意到,OP正在递归调用LoadData,实际上从未释放旧的DataTable。 - Panagiotis Kanavos
啊,这很有道理。他重构时要记住这一点。 - maniak1982
在真正的程序中,它不会被递归调用,而是由一些目录监视逻辑触发。这段代码只是用于测试。对于混淆感到抱歉。 - mhan0125

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