我有一个复杂的数据库转换控制台应用程序,可以从旧数据库中读取,执行大量操作并放入新数据库。
我的内存问题不断升级,我的内存使用情况(在任务管理器中监控)不断攀升,最终导致进程停止运行。
我已将其归结为最简单的测试POC,以尝试了解正在发生的事情。
for (int i = 0; i < 100000; i++)
{
TestObj testc = new TestObj
{
myTest = "testing asdf"
};
}
public class TestObj
{
public string myTest;
}
我的想法是,在循环中创建的每个testc
都不会在迭代结束后继续存在,但是内存跟踪它的方式似乎是应用程序保留在{的每个实例上{1}}。
我做了大量的研究和实验,但我觉得我在这里缺少一些东西。我不应该能够运行它并且内存利用率保持不变吗?
答案 0 :(得分:4)
你错过了关于垃圾收集的一个关键事项:GC在它需要之前不会运行。所以,是的,尽管它很好地清理了你,但是在需要更多内存之前它仍然不会这样做。
答案 1 :(得分:3)
如果你使用结构代替生活会更容易......
for (int i = 0; i < 100000; i++)
{
TestObj testc = new TestObj
{
myTest = "testing asdf"
};
}
public struct TestObj
{
public string myTest;
}
这仍然需要分配字符串,但结构将无法生存。这取决于你的班级真正的样子,如果你有很多价值类型,这将有很大帮助。如果你有一堆字符串/参考值仍然有问题。
否则您可以执行以下操作:
for (int i = 0; i < 100000; i++)
{
// do your work...
// then every 1k cycles, see if we have > 100mb allocated
// and force the GC to free the memory
if(i % 1000 == 0 && GC.GetTotalMemory(false) > 100000000)
GC.Collect();
}
注意:这是一件丑陋的“hacky”事情;但是,有时它是解决问题的最快方法。
<强>更新强>
另外,您需要确保没有按下LOH(Large object heap),因为这可能是内存争用的来源。作为一般规则,将字符串byte []等保持在85kb以下。这意味着字符串需要的长度少于42k字符。
答案 2 :(得分:2)
您可能需要了解C#使用“垃圾收集”来管理对象的生命周期。
你的循环没有给垃圾收集器实际处理对象的机会,因此它们会挂在内存中,直到编译器确定最适合处理它们的时刻为止。
<强>更新强>
我同意Sunny的观点,垃圾收集很可能不是你的问题。增加内存使用量的唯一方法可能会影响程序的执行速度,如果你已达到机器将大量RAM交换到磁盘的程度。
我建议您需要分析数据库交互。程序在什么时候开始减速?数据库服务器上的磁盘队列长度是否在增长?它一次尝试执行多少个查询?
这里有两种可能性。首先是减速通过电线发生拉大量数据。第二个是减速正在通过网络发生推送大量数据。
无论是哪种,请查看所涉及的特定服务器。它可能无法支持您要求的响应时间。
答案 3 :(得分:1)
当第1代活物体已满时,您的测试对象将被清除掉。或者当有人拨打GC.Collect()
时。试着打电话给你,你会发现什么都不会长大。
GC不会在.NET中没有人引用后立即清理内存。而且你的TestObject在内存消耗方面非常害羞,因为堆中只有一个字符串的共享实例。因此,您可以创建其中许多并且GC不会干扰。它们太小而无法发挥作用。
答案 4 :(得分:1)
您可以使用使用,(对于实施 IDisposable 界面的任何类型)
using(var conn = new SqlConnection ())
{
}
不要让SqlConnection保持比实际需要更长的时间,使用 WeakReference 类进行内部内存表示以及临时序列化/去实现存储。使用数据缓存,中介模式,观察者模式
答案 5 :(得分:0)
循环的每次迭代都会在内存中创建一个sizeof(TestObj)的新对象。最终结果是当你离开for循环时,内存使用量将是sizeof(TestObj)* 100000;由于您的对象不会超出范围(根据您的代码),因此它们将不会被GCed。
如果您关心内存,可以将TestObj放在using语句中(如果您的测试对象实现了IDisposible,则使用(obj = new TestObj())。如果您的处理可以使用,您也可以尝试使用多线程方法多线程方式,并在每次迭代时产生一个工作线程.TestObj可能会在收集线程的同一时间收集,也可能加快应用程序完成所有工作所需的总时间。
答案 6 :(得分:0)
垃圾收集不是确定性的,因此除非您明确调用GC.Collect()
,否则您不能期望立即看到结果。接下来,如果您正在使用ADO.NET,则需要确保处理每个实现IDisposable
的对象。不这样做会不必要地延迟收集。
此外,请确保您的简化示例真正模仿实际代码中发生的情况。例如,如果您的对象将其方法附加为其他长生命对象的事件处理程序,则只要此单个对象存在,它们将通过此引用保持活动状态。
要检查你的记忆的实际状态,某个点上所有生物的列表,以及让它们保持活着的实际参考,WinDbg with SoS extensions是一个不可替代的工具。
以您的示例为例,垃圾收集器无法清除最终。
您没有创建新的String
实例或除测试对象之外的任何内容。
由于.NET中的string interning,您的应用程序中只存在一个字符串常量,并且您的测试对象仅获取其引用(而不是副本)。
您的对象在堆上只占用16个字节(在32位环境中)
至少12个字节加上单个字符串引用构成一个16字节的大对象。这个很小,即使有一百万个。
循环创建了一堆小的,短暂的对象,它们不会创建任何新的引用,也不会附加任何事件处理程序。 GC非常善于收集这类物品,但只在有需要时收集它们。
答案 7 :(得分:0)
我要尝试的第一件事是通过添加方法将循环内部的代码与循环代码分开。
public void DoProcessing()
{
for (int i = 0; i < 100000; i++)
{
ProcessItem();
}
}
private void ProcessItem()
{
TestObj testc = new TestObj
{
myTest = "testing asdf"
};
}
public class TestObj
{
public string myTest;
}
答案 8 :(得分:0)
我会尝试:
TestObj testc;
for (int i = 0; i < 100000; i++)
{
testc = new TestObj
{
myTest = "testing asdf"
};
}
或者像Artur所说的那样,如果你的班级是IDisposable,则使用声明。
答案 9 :(得分:0)
我的猜测是你看到被吃掉的内存不是托管内存。这是关键问题:GC只知道托管内存。如果您正在处理在托管世界之外具有“足迹”的其他对象 - 例如。文件,COM对象,数据库连接,窗口等 - 这些将占用该进程中的内存,但由于GC只知道其占用空间的托管部分,因此非托管部分可以在不使用GC的情况下增长和增长意识到需要一个集合。
或者换句话说,GC非常适合管理纯内存,但是在管理资源(文件,COM对象,HANDLE,窗口等)方面很糟糕 - 如果如果您正在使用它们,那么您可能需要在完成后立即关闭/处置或以其他方式清理它们,而不是依赖GC。
你给对象+字符串的例子是纯粹的管理,这里没有资源,所以很可能它会达到某个上限,集合将启动,它会平稳,但不会使系统变慢(在最不要太多!)。
您在循环中实际创建/使用了哪些对象?如果它们是外部资源的包装,例如数据库连接或类似,请检查它们是否实现了IDispose,然后使用.Dispose或using()模式,或者查看是否存在关闭/断开连接或其他方法来释放资源。