.NET SqlConnection 和 DataSet 的内存泄漏问题

3
我在一个运行SQL脚本并将结果转储到文件的服务程序中遇到了内存泄漏问题。运行产生大量结果行的查询后,进程的内存使用量每次增加50+ MB,并且不会下降。
以下是打开连接并检索结果的代码:
using (var conn = new SqlConnection(DataSourceInfo.ConnectionString))
{
    conn.Open();

    var scmd = new SqlCommand(query_string, conn);
    scmd.CommandTimeout = 86400;

    var writer = dest.GetStream(); //the writer is disposed of elsewhere

    using (var da = new SqlDataAdapter(scmd))
    using (var ds = new DataSet())
    {
        da.Fill(ds);
        var table = ds.Tables[0];
        var rows = table.Rows;

        if (TaskInfo.IncludeColNames.Value)
        {
            object[] cols = new object[table.Columns.Count];

            for(int i = 0; i < table.Columns.Count; i++)
                cols[i] = table.Columns[i];

            LineFormatter(writer, TaskInfo.FieldDelimiter, null, false, cols);
            writer.WriteLine();
        }

        foreach(System.Data.DataRow r in rows)
        {
            var fields = r.ItemArray;

            LineFormatter(writer, TaskInfo.FieldDelimiter, TaskInfo.TextQualifier, TaskInfo.TrimFields.Value, fields);
            writer.WriteLine();
        }
    }
}

我使用WinDbg和sos.dll,在执行完成后,进程有足够的时间进行GC,列出了按类型排名前几的对象:

79333470      101       166476 System.Byte[]
65245dcc      177      3897420 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
0015e680     5560      3968936      Free
79332b9c      342      3997304 System.Int32[]
6524508c   120349      7702336 System.Data.DataRow
793041d0      984     22171736 System.Object[]
7993bec4       70     63341660 System.Decimal[]
79330a00  2203630     74522604 System.String

第二列是对象数量,第三列是总大小。
不应该有任何未处理的System.Data.DataRow对象。看起来它们被泄漏了,但我不确定原因在哪里。
我做错了什么?
注意:之前的版本使用SqlDataReader检索行数据,但这种方法缺乏获取列标题的方式(据我所知),并且在DataSet和SqlDatReader之间共享数据集会在某些查询上静默失败。我不记得那个版本有内存泄漏问题。

对我来说看起来很奇怪。只是猜测,你尝试过将你的 SqlCommand 包装在 using 语句中吗? - kbrimington
你尝试过自己调用GC.Collect()来确保DataRows首先是可回收的吗?如果我没记错,GC只有在有内存压力时才进行垃圾回收。 - user47589
由于某些原因,我认为SqlCommand不是可处理的。我会将其放在using子句中,但问题似乎与查询大小成比例。我认为GC实际上并没有发生的建议可能是正确的。我会尽快检查它。 - Chris Smith
4个回答

2

我检查了其中几个,但没有得到任何结果。可能是像其他地方提到的那样,集合实际上并没有发生。当我回到开发机器时,我会通过手动GC来检查它。 - Chris Smith

2
除非LineFormatter在整个程序的生命周期中执行某些操作来保留引用,否则我在这里看不到任何问题。
你对垃圾回收器的工作原理做了一些大胆的假设。据我所知,它是基于内存压力而不是时间来工作的。如果你感到非常担心,可以在代码中运行GC.Collect(),看看是否能降低内存使用量,但我永远不会在生产代码中调用GC.Collect(),只需将其作为测试。
还要确保你不依赖任务管理器告诉你.NET堆中保留了多少内存。你应该查看PerfMon中的性能计数器,以检查托管世界中发生了什么。

运行手动GC.Collect()确实释放了内存--从184MB私有字节到33MB(接近起始位置)。那么,如果我不应该在生产代码中使用GC.Collect(),我应该怎么做呢?这个进程每天只运行几分钟。我不希望它占用SQL Server或其他缓存中更好使用的内存。CLR保留所有未引用内存的好处是什么? - Chris Smith
当GC.Collect()执行时,它实际上会遍历并整理内存。这可能会对负载产生相当大的影响,因此垃圾收集器只在内存压力下才执行。如果进程确实只运行了几分钟的话,我会将其作为计划任务放入。至于SQL Server,您应该配置它使用固定数量的RAM。通过这样做,SQL Server将对其他应用程序施加内存压力,而不是相反。 - Dave Markle
该服务等待外部事件以响应。尽管它每天只运行几分钟,但作为计划任务它真的无法工作。它必须一直保持开启状态。不幸的是,我无法控制SQL Server配置。该服务应该静默地坐在后台,不影响同一台机器上的其他服务。我最终做的是,在内部活动工作者数量达到0后1分钟手动运行GC。手动GC期间的CPU使用率可以忽略不计。 - Chris Smith
如果你被迫处于这种位置,那么这不是一个太糟糕的解决方案。但是,如果你的配置使得其他应用程序可以对你的SQL Server施加内存压力,你应该将这个问题上报给更高层级的管理人员。这是一件坏事。 - Dave Markle

0

追踪内存泄漏的最佳方法是使用分析器,例如Nant或.Net Memory Profiler。我认为两者都至少有15天的试用期,这足以学习您需要的内容并诊断内存泄漏。

我使用过.Net Memory Profiler。它非常擅长准确地追踪被持有的内容以及从AppDomain或任何静态对象到达泄漏内存的路径。它通过运行您的应用程序并抓取元数据来工作;您使用分析器拍摄一个快照,执行一个会泄漏内存的操作,然后拍摄第二个快照并进行比较。您可以隔离出两个快照之间的差异,并按大小排序,因此您可以很快地找到问题所在。这是一个非常好的工具!


0

你可能需要将你的SqlCommand放在using块中,或手动释放它。


在这个例子中,我忘记了处理SqlCommand。在原始代码中,它是在其他地方创建和处理的。结果发现集合没有被处理;请参考已接受的答案。 - Chris Smith

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