.NET SqlConnection 和 DataSet 的内存泄漏
我在运行 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 之间共享数据集会在某些查询上默默失败。我不记得那个版本有内存泄漏问题。
I'm having a memory leak issue in a service program that runs SQL scripts and dumps the results out to files. After running queries that produce many result rows, the memory usage of the process goes up by 50+ MB each time and doesn't come down.
Here is the code that opens the connection and retrieves the results:
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();
}
}
}
I used WinDbg with sos.dll to list the top objects by type after execution had completed and the process had plenty of time to 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
The second column is the number of objects and the third is the total size.
There shouldn't be any System.Data.DataRow objects outstanding. It looks like they're being leaked somehow, but I'm not sure how.
What am I doing wrong?
Note: a previous version used SqlDataReader to retrieve the row data, but that approach lacked a way to get the column headers (that I know of) and sharing the data set between the DataSet and SqlDatReader would silently fail on some queries. I do not remember that version having the memory leak problem.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
除非 LineFormatter 正在做一些事情来在程序的生命周期中保留引用,否则我认为这里没有问题。
您正在对垃圾收集器的工作原理做出一些重大假设。 AFAIK,它的工作原理是基于内存压力,而不是时间。如果您真的感觉很偏执,您可以在代码中运行 GC.Collect() ,看看这是否会降低内存使用量,但我永远不会在生产代码中调用 GC.Collect() ——只是将其作为测试。
还要确保您不依赖任务管理器来告诉您 .NET 堆中保留了多少内存。您应该查看 PerfMon 中的性能计数器检查托管世界中正在发生的事情。
Unless LineFormatter is doing something to hold onto references for the life of the program, I see no issue here.
You're making some big assumptions about how the garbage collector works. AFAIK, it works based on memory pressure, not time. If you were feeling really paranoid, you could run a GC.Collect() in the code and see if that brings the memory usage down, but I would never call GC.Collect() in production code -- just do this as a test.
Also make sure you're not relying on the Task Manager to tell you about how much memory is being reserved in the .NET heap. You should instead be looking at the performance counters in PerfMon to examine what's going on in the managed world.
选择一个 DataRow 并使用
!gcroot
查看谁在行中保留引用。请参阅跟踪托管内存泄漏(如何查找 GC 泄漏)。Pick a DataRow and use
!gcroot
to see who's keeping a reference at the rows. See Tracking down managed memory leaks (how to find a GC leak).追踪内存泄漏的最佳方法是使用探查器,例如 Nant 或 .Net Memory Profiler。我认为两者都至少有15天的试用期,这足以了解你需要什么并诊断内存泄漏。
我使用过.Net Memory Profiler。它非常擅长准确追踪所持有的内容,以及从 AppDomain 或任何静态对象获取泄漏内存的路径。它的工作原理是运行您的应用程序并获取元数据;您拍摄快照(使用分析器),运行一个泄漏内存的操作,然后拍摄第二个快照并进行比较。您可以隔离两个快照之间的差异,并按大小排序,这样您就可以快速解决问题。非常好的工具!
The best way to track down a memory leak is with a profiler, such as Nant or .Net Memory Profiler. I think both have at least a 15 day trial period, which is enough to learn what you need and to diagnose the memory leak.
I've used .Net Memory Profiler. It's very good at tracking down exactly what is being held, and what the paths are to get to the leaked memory from AppDomain or from any static objects. It works by running your app and grabbing metadata; you take a snapshot (with the profiler), run one operation that leaks emory, then take a second snapshop and compare. You can isolate what's different between the two snapshots, and sort by size, so you close in very quickly on the problem. Very good tool!
您可能需要将 SqlCommand 放入 using 块中,或手动处置它。
You might need to put your SqlCommand in a using block, or dispose it manually.