【问题标题】:DataTable does not release memoryDataTable 不释放内存
【发布时间】:2015-08-19 06:29:46
【问题描述】:

我有一个数据加载过程,将大量数据加载到DataTable然后做一些数据处理,但是每次作业完成时DataLoader.exe(32位,有1.5G内存限制)并没有释放所有正在使用的内存。

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

  1. DataTable.Clear() 然后调用 DataTable.Dispose() (释放大约 800 MB 内存,但每次数据加载作业完成时仍会增加 200 MB 内存,经过 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();
        }
    }
  }
}

【问题讨论】:

  • DataTable 本身没有实现Dispose(),它从它的父级MarshalByValueComponent 获取方法,MarshalByValueComponent 所做的唯一两件事就是调用Site.Container.Remove(this) 如果你有数据表里面一个ISite (您的代码示例中没有)并引发Disposed 事件。它不会释放任何资源。
  • 我不知道,但我敢打赌您正在释放您的对象,但您遇到了内存碎片问题。编辑:哦,在没有附加调试器的情况下运行你的程序,你会看到不同的行为。附加调试器时,GC 的行为非常不同。
  • @ScottChamberlain 谢谢。但在我的情况下,这个数据加载是在一个目录中观看 exe。为了防止这个 OutOfMemoery 异常,我该怎么办?
  • @ScottChamberlain 好的,我会在发布模式下尝试。
  • 在没有调试器的情况下运行更重要,在发布模式下运行会产生较小的变化,但在没有调试器的情况下运行会对行为进行重大更改。

标签: c# memory memory-management datatable


【解决方案1】:

您的主要问题是垃圾收集器的行为会有所不同,具体取决于您是在调试还是在没有调试器的情况下处于发布模式。

在带有调试器的调试版本或发布版本中,所有对象的生命周期都会延长到方法的整个生命周期。这意味着table 在您完成LoadData 方法之前不能被GC 回收。这就是为什么你总是内存不足的原因。

如果您将程序更改为发布模式并在没有调试器的情况下运行它,那么一旦您传递对对象的最后一个引用,变量table 在您的代码路径中指向的对象就可以进行垃圾回收,并且您释放内存。

GC 在“可调试情况”期间改变其行为的原因是认为调试器本身持有对当前执行代码范围内的所有变量的引用。如果没有,您将无法在监视窗口中查看变量的值或将鼠标悬停在它上面。因此,在变量超出范围或覆盖变量之前,您不能“传递对对象的最后一个引用”。

有关该过程的更多详细信息,请参阅博客帖子 On Garbage Collection, Scope and Object Lifetimes

【讨论】:

    【解决方案2】:

    如果在函数外请求重复时移动部件,内存会被正确释放(仅测试方法 1(清除和处置)):

    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();
        }
    }
    

    对象的内存可能在超出范围时被释放

    【讨论】:

    • 在没有调试器的情况下以发布模式运行会解决这个问题,OP 对此感到厌烦,但仍然出现 OOM 错误。
    • 我觉得codroipo的方式是对的,每次运行后都会释放内存。
    • @ScottChamberlain 通过保留对 DataTable 的引用,OP 阻止了 GC 收集任何剩余数据,例如更改跟踪数据。在发布模式下运行不会改变这一点。简单的解决方案是 codroipo 提出的 - 不要重用变量,将其设为空或更好,将其移动到不同的方法中,以便在超出范围时可以收集它
    • @PanagiotisKanavos 我不是在谈论发布模式或调试模式,我是在谈论 JITer 在连接调试器时如何处理对象的生命周期,而不是连接调试器。请阅读On Garbage Collection, Scope and Object Lifetimes它详细解释了差异。
    • @PanagiotisKanavos 不,您没有抓住重点,如果您没有附加调试器并且处于发布模式,则哪些是活动的,哪些不会改变的规则。如果您在发布模式、x86 模式下构建 OP 的程序,并且在没有调试器的情况下运行它,我无法让它获取内存,它总是在每个 GC.Collect() 之后回到 11 MB。在调试器中运行它或在调试模式下运行它会使它像 OP 看到的那样爆炸。
    【解决方案3】:

    最后我发现这个Data table not release memory bug是Oracle大容量复制造成的。以防万一有人遇到同样的问题。请参阅以下帖子以供参考

    OracleBulkCopy Memory Leak(OutOfMemory Exception)

    【讨论】:

      【解决方案4】:

      实际上并没有一种方法可以像使用没有内存管理的代码那样强制 C# 释放内存。它有助于理解 .NET 垃圾收集器的工作原理。基本上 .NET 应用程序中的内存使用量上升到触发垃圾收集的三个条件之一。我在以下问题的答案中描述了该过程:

      Cleaning up variables in methods

      避免OutOfMemory 异常的一种方法是使用MemoryFailPoint 类,它允许您设置一个失败点,超过该点会抛出InsufficientMemoryException,让您有机会减慢进程直到另一个工作人员线程可用。我不确定这是否是您想要尝试的东西,但您可以使用它:

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

      【讨论】:

      • 一个更简单的解决方法是简单地将变量设为空并创建一个新实例。通过保持同一个对象处于活动状态,内部结构永远不会被释放。
      • 用户在执行此操作时看到了OutOfMemory 错误。在狭窄范围内使用对象是个好主意,但正如我在上面所指出的,仅将其设置为 null 不足以触发垃圾收集。
      • 没说有。不设置为 null 虽然可以防止垃圾收集。无论如何,我刚刚注意到,OP 正在递归调用 LoadData,基本上从不释放旧的 DataTables。
      • 啊,有道理。在他重构时要记住一件好事。
      • 在实际程序中它不是递归调用的,它是由一些目录监视逻辑触发的。此代码仅用于测试。很抱歉造成混乱。
      猜你喜欢
      • 2017-06-29
      • 2015-01-01
      • 2011-06-30
      • 2016-09-14
      • 2012-05-15
      • 2018-01-26
      • 2011-09-30
      • 2012-03-18
      • 2011-08-10
      相关资源
      最近更新 更多