当您确保所有句柄和实现的东西IDispose
都被释放时,托管系统中是否有可能泄漏内存?
会不会有一些变量被遗漏的情况?
当您确保所有句柄和实现的东西IDispose
都被释放时,托管系统中是否有可能泄漏内存?
会不会有一些变量被遗漏的情况?
事件处理程序是非明显内存泄漏的常见来源。如果您从 object2 订阅 object1 上的事件,然后执行 object2.Dispose() 并假装它不存在(并从您的代码中删除所有引用),则 object1 的事件中有一个隐式引用将阻止 object2垃圾收集。
MyType object2 = new MyType();
// ...
object1.SomeEvent += object2.myEventHandler;
// ...
// Should call this
// object1.SomeEvent -= object2.myEventHandler;
object2.Dispose();
这是一个常见的泄漏案例——忘记轻松取消订阅事件。当然,如果 object1 被收集,object2 也会被收集,但在此之前不会。
我认为 C++ 风格的内存泄漏是不可能的。垃圾收集器应该考虑这些。可以创建聚合对象引用的静态对象,即使这些对象不再使用。像这样的东西:
public static class SomethingFactory
{
private static List<Something> listOfSomethings = new List<Something>();
public static Something CreateSomething()
{
var something = new Something();
listOfSomethings.Add(something);
return something;
}
}
这显然是一个愚蠢的例子,但它相当于托管运行时内存泄漏。
正如其他人指出的那样,只要内存管理器中没有实际的错误,不使用非托管资源的类就不会泄漏内存。
您在 .NET 中看到的不是内存泄漏,而是永远不会被释放的对象。只要垃圾收集器可以在对象图上找到一个对象,它就不会被释放。因此,如果任何地方的任何活对象都引用了该对象,它就不会被释放。
事件注册是实现这一目标的好方法。如果一个对象注册了一个事件,它注册的任何东西都有对它的引用,即使你消除了对该对象的所有其他引用,直到它取消注册(或者它注册的对象变得不可访问)它都会保持活动状态。
因此,您必须注意在您不知情的情况下注册静态事件的对象。例如,一个漂亮的功能ToolStrip
是,如果您更改显示主题,它将自动在新主题中重新显示。它通过注册静态SystemEvents.UserPreferenceChanged
事件来完成这个漂亮的功能。当您更改 Windows 主题时,会引发事件,并且所有ToolStrip
正在侦听该事件的对象都会收到通知,告知您有新主题。
ToolStrip
好的,假设您决定在表单上丢弃 a :
private void DiscardMyToolstrip()
{
Controls.Remove("MyToolStrip");
}
你现在有一个ToolStrip
永远不会死的。即使它不再出现在您的表单上,每次用户更改主题时,Windows 都会尽职尽责地告诉其他不存在的人ToolStrip
。每次垃圾收集器运行时,它都会认为“我不能把那个对象扔掉,UserPreferenceChanged
事件正在使用它”。
这不是内存泄漏。但也可能如此。
这样的事情使内存分析器变得非常宝贵。运行内存分析器,你会说“这很奇怪,ToolStrip
堆上似乎有一万个对象,尽管我的表单上只有一个。这是怎么发生的?”
哦,如果您想知道为什么有些人认为属性设置器是邪恶的:要让 aToolStrip
从UserPreferenceChanged
事件中注销,请将其Visible
属性设置为false
.
委托可能会导致不直观的内存泄漏。
每当您从实例方法创建委托时,对该实例的引用都会存储在该委托“中”。
此外,如果您将多个委托组合成一个多播委托,那么只要在某处使用该多播委托,您就会拥有对众多对象的一大堆引用,这些对象不会被垃圾收集。
如果您正在开发一个 WinForms 应用程序,一个微妙的“泄漏”是Control.AllowDrop
属性(用于启用拖放)。如果AllowDrop
设置为“true”,CLR 仍会通过System.Windows.Forms.DropTarget
. 要解决此问题,请确保您Control
的 'sAllowDrop
属性设置为false
您不再需要它时,CLR 将处理其余的事情。
.NET 应用程序中内存泄漏的唯一原因是对象仍在被引用,尽管它们的生命周期已经结束。因此,垃圾收集器无法收集它们。它们成为长寿的对象。
我发现通过订阅事件而不在对象的生命结束时取消订阅它很容易导致泄漏。
如前所述,保留引用将导致内存使用量随着时间的推移而增加。进入这种情况的一个简单方法是使用事件。如果您有一个长寿命的对象,并且您的其他对象正在侦听某个事件,如果从未删除侦听器,那么长寿命对象上的事件将在不再需要这些其他实例后很长时间内保持它们的活性。
您可能会发现我的新文章很有用:如何检测和避免 .NET 应用程序中的内存和资源泄漏
反射发射是另一个潜在的泄漏源,例如内置对象反序列化器和花哨的 SOAP/XML 客户端。至少在框架的早期版本中,从不卸载从属 AppDomains 中生成的代码......
您不能在托管代码中泄漏内存是一个神话。诚然,这比非托管 C++ 中要困难得多,但有一百万种方法可以做到这一点。持有引用、不必要的引用、缓存等的静态对象。如果你以“正确”的方式做事,你的许多对象直到比必要的时间晚得多才会被垃圾收集,这在我看来也是一种内存泄漏,以实际而非理论的方式。
幸运的是,有一些工具可以帮助您。我经常使用 Microsoft 的CLR Profiler——它不是有史以来最友好的工具,但它绝对非常有用并且是免费的。
一旦对对象的所有引用都消失了,垃圾收集器将在下一次传递时释放该对象。我不会说泄漏内存是不可能的,但这相当困难,为了泄漏,你必须在没有意识到的情况下引用一个坐在周围的对象。
例如,如果您将对象实例化为列表,然后在完成后忘记将它们从列表中删除并且忘记释放它们。
不是真正的内存泄漏,但在使用大对象时很容易耗尽内存(如果我没记错的话,大于 64K)。它们存储在 LOH 上,并且没有进行碎片整理。因此,使用这些大对象并释放它们会释放 LOH 上的内存,但 .NET 运行时不再为该进程使用该空闲内存。因此,只需使用 LOH 上的几个大对象,您就可以轻松地用完 LOH 上的空间。微软知道这个问题,但我记得现在正在计划解决这个问题。
如果未正确清理非托管资源,则可能会发生泄漏。实现IDisposable的类可能会泄漏。
但是,常规对象引用不像低级语言那样需要显式内存管理。
虽然框架中的某些东西可能存在泄漏,但更有可能的是您有一些东西没有被正确处理,或者某些东西阻止了 GC 处理它,IIS 将是这个问题的主要候选者。
请记住,并非 .NET 中的所有内容都是完全托管的代码、COM 互操作、文件 io(如文件流、数据库请求、图像等)。
我们前段时间遇到的一个问题(IIS 6 上的 .net 2.0)是我们将创建一个映像然后将其处理掉,但 IIS 暂时不会释放内存。
唯一的泄漏(除了运行时中可能存在的错误,尽管由于垃圾收集而不太可能)将用于本机资源。如果您 P/Invoke 到本机库中,该库代表您的托管应用程序打开文件句柄或套接字连接或任何内容,并且您从未显式关闭它们(并且不在处置器或析构器/终结器中处理它们),您可以有内存或资源泄漏,因为运行时无法为您自动管理所有这些。
但是,如果您坚持使用纯托管资源,那么您应该没问题。如果您在没有调用本机代码的情况下遇到任何形式的内存泄漏,那么这就是一个错误。
在我的上一份工作中,我们使用了一个像筛子一样泄漏的第 3 方 .NET SQLite 库。
在每次都必须打开和关闭数据库连接的奇怪情况下,我们进行了大量快速数据插入。第 3 方库做了一些我们应该手动打开的相同连接,但没有记录它。它还在我们从未找到的地方保存了参考资料。结果是打开的连接数量是预期的 2 倍,而只有 1/2 的连接被关闭。由于引用被保留,我们有内存泄漏。
这显然与经典的 C/C++ 内存泄漏不同,但从所有意图和目的来看,这对我们来说都是一个问题。
自我提醒如何找到内存泄漏:
常见的内存泄漏问题:
如果它被认为是内存泄漏,也可以使用这种代码来实现:
public class A
{
B b;
public A(B b) { this.b = b; }
~A()
{
b = new B();
}
}
public class B
{
A a;
public B() { this.a = new A(this); }
~B()
{
a = new A(this);
}
}
class Program
{
static void Main(string[] args)
{
{
B[] toBeLost = new B[100000000];
foreach (var c in toBeLost)
{
toBeLost.ToString(); //to make JIT compiler run the instantiation above
}
}
Console.ReadLine();
}
}
使用 .NET XmlSerializer 时可能会发生内存泄漏,因为它使用了不会被释放的非托管代码。
请参阅文档并在此页面上搜索“内存泄漏”:
在控制台或 Win 应用程序中创建一个Panel
对象(panel1),然后添加 1000PictureBox
并设置其Image
属性,然后调用panel1.Controls.Clear
. 所有 PictureBox 控件仍在内存中,GC
无法收集:
var panel1 = new Panel();
var image = Image.FromFile("image/heavy.png");
for(var i = 0; i < 1000;++i){
panel1.Controls.Add(new PictureBox(){Image = image});
}
panel1.Controls.Clear(); // => Memory Leak!
正确的做法是
for (int i = panel1.Controls.Count-1; i >= 0; --i)
panel1.Controls[i].Dispose();
调用 Clear 方法不会从内存中删除控制句柄。您必须显式调用 Dispose 方法以避免内存泄漏
小功能有助于避免“内存泄漏”。因为垃圾收集器在函数结束时释放局部变量。如果函数很大并且占用大量内存,您必须自己释放占用大量内存且不再需要的局部变量。类似的全局变量(数组、列表)也很糟糕。
在创建图像而不是处理它们时,我在 C# 中遇到了内存泄漏。这有点奇怪。人们说你必须在每个拥有它的对象上调用 .Dispose() 。但是图形 C# 函数的文档并不总是提到这一点,例如函数 GetThumbnailImage()。我认为 C# 编译器应该警告你。