我一直致力于从数据库中提取大量记录的数据导出程序。其中一个步骤涉及将RTF文本字符串转换为纯文本,这最终导致用户对象在运行时发生内存泄漏。任务管理器将显示的一个列是“USER对象” - 当它达到~10,000时,程序将耗尽分配空间,程序将出现“错误创建窗口句柄”
这种情况正在发生,因为我没有在方法结束时处理我的对象。
我的问题是,为什么没有C#/。net为我处理它?
这是一个快速重现漏洞的代码示例。将代码放入Winforms应用程序并按下按钮使其循环通过内存浪费。
private void wasteMemory()
{
System.Windows.Forms.RichTextBox rtfBox = new System.Windows.Forms.RichTextBox();
//RTF text that reads "Hello World"
rtfBox.Rtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}} {\\colortbl ;\\red0\\green0\\blue0;} \\viewkind4\\uc1\\pard\\cf1\\fs29 Hello World} ";
//If line below is commented out, User Objects grow out of control.
//rtfBox.Dispose();
}
private void button1_Click(object sender, EventArgs e)
{
for (int i = 1; i < 100000; i++)
{
wasteMemory();
}
}
我的理解是,当方法完成时,处理在其侧面创建的任何对象的方法范围。我预计rtfBox会被处理掉,但事实并非如此。
答案 0 :(得分:6)
Dispose方法是.NET方式,为具有本机资源的对象提供清理的机会。它有点像C ++中的析构函数/删除 - 虽然不是真的。如果你没有在实现IDisposable的对象上调用Dispose,那就是一个bug,很可能会导致内存泄漏。最好这样做:
using(System.Windows.Forms.RichTextBox rtfBox = new System.Windows.Forms.RichTextBox())
{
//RTF text that reads "Hello World"
rtfBox.Rtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}} {\\colortbl ;\\red0\\green0\\blue0;} \\viewkind4\\uc1\\pard\\cf1\\fs29 Hello World} ";
}
使用块的行为与您期望的完全相同。你可以把它想象成C ++中用于堆栈中对象的方法范围。
我的理解是,当方法完成时,处理在其侧面创建的任何对象的方法范围。我预计rtfBox会被处理掉,但事实并非如此。
不,这根本不是真的 - 或者与大多数其他垃圾收集的语言有关。如果您认识到这里的对象是动态分配的(即非常像指针),那么对于像C ++这样的语言来说甚至都不是这样,因为当指针超出范围时,动态分配的内存不会被清除:你有显式调用delete。在.NET中,对象将被最终化,析构函数将被调用,并且当垃圾收集器到达它时将调用dispose,但在此之前不会。超出范围只会向垃圾收集器发出信号,表明相关对象有资格被收集。但是,任何具有资源的东西,例如本机代码,文件句柄或其他IDisposable实现对象,都应该通过处理来处理。用户完成后立即处理()以避免内存泄漏。
有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/system.idisposable.aspx
答案 1 :(得分:6)
到目前为止,每个答案都是不完整的。是的,确实必须清理非托管资源,但实现IDisposable的类已经这样做了。这不是重点。
在正确实现IDisposable的类中,如果没有显式或隐式地处理对象,那么它将在垃圾收集的终结器阶段处理。但是,当对象超出范围时,此过程不会立即发生。 gc可能需要几分钟甚至几小时才能运行。
这里的问题是,如果你不自己调用Dispose()(或通过将它包装在using语句中隐式调用Dispose()),那么(如果它由类正确实现),该对象将被释放,直到垃圾收集器运行,这可能需要相当长的时间。
这意味着在垃圾回收器处理未引用的对象之前,您可能会耗尽非托管资源。这正是你遇到的问题。
自己调用Dispose()可以确保非完整对象在完成后立即得到清理,而不是在GC到达它时。
把它想象成一个图书馆。有人检查了一本书,书架上有5本。当其他人查看这个图书馆时,有些人会把它们归还..但是他们不会立即放在架子上,他们会坐在回收箱里,直到有人到处检查并重新安置它们。
调用Dispose就像将书交给图书管理员一样,让他们立即检查,然后将其放回架子上,以便下一个人可以使用它。
答案 2 :(得分:2)
.NET仅提供内存的自动垃圾收集。它对手柄一无所知,所以你必须自己清理它们。这就是IDisposable模式和终结器的用途。
答案 3 :(得分:1)
.NET运行时及其垃圾收集器只有在对象是托管对象时才会处置它。您必须处理非托管对象。这是因为非托管对象可以使用不受运行时控制的资源。如果运行时自己关闭了这些对象,那么系统内存中就会有很多垃圾,而其他进程无法处理这些垃圾。
您可能需要查看此问题:What is meant by "managed" vs "unmanaged" resources in .NET?
答案 4 :(得分:0)
垃圾收集器只响应内存压力。如果您有其他受限资源,例如GUI句柄,则需要确保正确处理它们。垃圾收集器没有声称要为你处理。
这正是IDisposable
的用途。
答案 5 :(得分:0)
垃圾收集和您在代码中调用的dispose方法之间存在差异。垃圾收集器将收集不再具有任何引用的对象,但该集合位于.NET运行时的域中。如果存在与对象关联的任何本机资源,则垃圾收集器将无法清除这些资源。
IDisposable
界面旨在解决此问题。期望IDisposable
对象的用户手动调用Dispose
方法,以便为对象提供自我清理的机会(例如,它可能使用的任何本机资源)。
查看C#中的“using”语句,这将使IDisposable
个对象的使用更加容易。
进一步阅读:
IDisposable:http://msdn.microsoft.com/en-us/library/system.idisposable.aspx
答案 6 :(得分:0)
.NET只知道.NET分配的内存。这意味着如果任何代码调用分配内存的“非托管”代码,那么
这就是.Dispose()存在的原因,因此您可以提前处理“非托管”内存。制作使用非托管内存的.NET对象的人应该实现IDisposable。
如果正确实现了非托管对象,那么它还将包含一个“终结器”~ClassName(),每当GC进行大整理时“应该”调用它。但是你永远不应该依赖被调用的终结器。如果一个过程突然关闭,或者对于那些不理解配置模式的开发人员来说,终结者只是在那里进行清理。