我继承了一项Windows服务,该服务通过远程处理接受请求,以分析数据库中潜在的大量数据。它通过将原始数据加载到数据集中,将数据解析为树状结构的对象,然后运行分析来检索原始数据。
我遇到的问题是,如果分析了一大堆数据,那么即使我积极地强制进行垃圾收集,也不会在分析完成后将所有内存都返回给系统。例如,如果分析了500MB的数据集,那么windows服务从~10MB(启动时的基线)到~500MB,然后在GC.Collect()之后下降到~200MB,并且永远不会降低,甚至一夜之间。恢复内存的唯一方法是停止并重新启动服务。但如果我进行一个小分析,服务从大约10MB到大约50MB,然后降到大约20MB。也不是很好,但在分析完成后,大小数据之间的最终利用率之间存在巨大差异。
这本身并不是内存泄漏,因为如果我反复运行大分析,每次完成时总内存会减少到大约200MB。
这是一个问题,因为Windows服务在共享服务器上运行,我不能让我的进程一直占用大量内存。如果它达到峰值然后在分析完成后再回落,那就没关系,但这种情况会出现并且部分下降到一个不可接受的高数字。典型的情况是运行分析,然后闲置几个小时。
不幸的是,这个代码库非常庞大,其中很大一部分被编码为使用专有数据访问层返回的数据表,因此使用备用方法加载数据不是一个选项(我希望我可以,加载将所有数据存入内存只是为了循环它是没有意义的。)
所以我的问题是:
1)为什么运行大型数据集会导致内存利用率降低到~200MB,但运行小型数据集会导致内存利用率降低到~20MB?它显然是以某种方式挂在数据集的各个部分上,我只是看不到它。
2)如果我循环遍历数据表的行,为什么会有所不同?(见下文)?
3)如何在分析完成后将内存恢复到合理的水平,而不会从根本上改变架构?
我创建了一个小型Windows服务/客户端应用程序来重现问题。我使用的测试数据库有一个包含一百万条记录的表,一个int PK和两个字符串字段。这是我尝试过的场景 - 客户端(控制台应用程序)通过远程循环远程调用LoadData十次。
1)doWork = true,garbageCollect = true,recordCount = 100,000。内存高达78MB,然后稳定在22MB。
2)doWork = false,garbageCollect = true,recordCount = 100,000。内存高达78MB,稳定在19MB。说真的,在没有做任何事情的情况下,还要多行3MB才能完成这些行吗?
3)doWork = false,garbageCollect = false,recordCount = 100,000。内存高达约178MB,然后稳定在78MB。强制垃圾收集显然正在做些什么,但还不足以满足我的需求。
4)doWork = false,garbageCollect = true,recordCount = 1,000,000。内存高达500MB,稳定在35MB。当数据集较大时,为什么它会稳定在更高的数字?
5)doWork = false,garbageCollect = true,recordCount = 1,000。它运行速度太快,无法看到峰值,但稳定在12MB左右。
public string LoadData(bool doWork, bool garbageCollect, int recordCount)
{
var dataSet = new DataSet();
using (var sqlConnection = new SqlConnection("...blah..."))
{
sqlConnection.Open();
using (var dbCommand = sqlConnection.CreateCommand())
{
dbCommand.CommandText = string.Format("select top {0} * from dbo.FakeData", recordCount.ToString());
dbCommand.CommandType = CommandType.Text;
using (var dbReader = new SqlDataAdapter(dbCommand))
{
dbReader.Fill(dataSet);
}
}
sqlConnection.Close();
}
// loop over the records
var count = dataSet.Tables[0].Rows.Count;
if (doWork)
{
foreach (DataRow row in dataSet.Tables[0].Rows) {}
}
dataSet.Clear();
dataSet = null;
if (garbageCollect)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
return string.Format("Record count is {0}", count);
}