如何解决C#中的内存泄漏问题?

10

我正在学习C#。据我所知,您必须正确设置才能使垃圾回收器按照应该的方式删除所有内容。我想从您这里学到多年积累的智慧。

我来自C++背景,并非常习惯代码味道和开发模式。我想了解C#中的代码味道是什么样子的。给我一些建议!

最好的获取删除内容的方法是什么?

如何确定您是否存在"内存泄漏"?


编辑:我正在尝试制定“内存管理必须始终做的事情”清单


非常感谢。

10个回答

18

C#使用托管内存,除了已分配的非托管资源之外,所有东西都会被垃圾回收。可以安全地假设托管类型始终会被垃圾回收,包括数组、类和结构体。可以随意执行“int[] stuff = new int[32];”并将其忘记。

如果在类中打开文件、数据库连接或任何其他非托管资源,请实现IDisposable接口,并在Dispose方法中释放非托管资源。

任何实现IDisposable接口的类都应该显式关闭或在Using块中使用(我认为很酷)。

using (StreamReader reader = new StreamReader("myfile.txt"))
{
   ... your code here
}

在这里,.NET会在 { } 范围外释放读取器。


1
那个“或”很令人困惑 - 即使是IDisposable对象也会被垃圾回收。GC对IDisposable一无所知。通常Dispose()调用SuppressFinalize,但这与此无关。 - Marc Gravell
好观点。因此,包含类被垃圾回收,但其资源被显式释放。我从未想过这一点。 - Dead account
3
“可以安全地假设托管类型总是会被垃圾回收。”这个说法是错误的。如果一个对象仍然可以被访问,那么它就不会被垃圾回收。程序员必须时刻谨记在不再需要时将根引用设置为 null。 - Cecil Has a Name

11

关于GC的第一件事是它是不确定性的;如果你想要资源快速清理,请实现IDisposable并使用using;这并不会清除托管内存,但可以在处理非托管资源和向前链时提供很大的帮助。

特别需要注意以下几点:

  • 大量固定(对GC的限制很大)
  • 大量终结器(通常不需要它们,会减慢GC)
  • 静态事件 - 保持许多大型对象图形的简单方式 ;-p
  • 昂贵对象已应清理但在长期存在的廉价对象上的事件
  • “捕获的变量”意外地保持着图形的存在

要调查内存泄漏,"SOS" 是最容易的路线之一;你可以使用 SOS 找到类型的所有实例以及能够访问它的内容等。


1
静态事件很危险,我同意。 - Quibblesome

4
我能想到的主要内存泄漏来源包括:
  • 保留对不再需要的对象的引用(通常是在某种集合中)。因此,在向您拥有引用的集合中添加的所有内容都将保留在内存中。

  • 存在循环引用,例如使用委托注册事件。即使您明确不引用对象,但由于其方法之一已注册为事件的委托,因此无法进行垃圾回收。在这些情况下,您需要记住在丢弃引用之前删除委托。

  • 与本地代码进行交互并未释放它。即使您使用实现终结器的托管封装器,CLR 通常也无法快速清除它们,因为它无法理解内存占用情况。您应该使用 using(IDisposable ){} 模式

1
“循环引用”并不是真正的问题- 问题在于两个对象中仍然可以有效地看到其中一个,而事件会使第二个对象保持活动状态。 - Marc Gravell

3
总的来说,在C#中越少担心内存分配,你就越好。我会让性能分析工具告诉我何时出现集合问题。
在C#中,你无法像在C++中那样创建内存泄漏。垃圾回收器始终会“保护你的后背”。你可以创建对象并保留对它们的引用,即使你从未使用过它们。这是要注意的代码气味。
除此之外:
  • 有一些关于集合频率的概念(出于性能原因)
  • 不要比你需要的时间长地保留对象的引用
  • 尽快处理实现IDisposable接口的对象(使用using语法)
  • 正确实现IDisposable接口

1

在考虑内存管理时,另一件需要考虑的事情是是否实现了任何观察者模式并且没有正确处理引用。

例如: 对象A监视对象B 如果从A到B的引用未通过属性释放,则对象B将被释放,GC将无法正确地处理该对象。因为事件处理程序仍然被分配,GC不会将其视为未使用的资源。

如果您正在处理少量对象,则可能与此无关。但是,如果您要处理数千个对象,则可能会导致应用程序的生命周期内逐渐增加内存。

有一些出色的内存管理软件应用程序可以监视应用程序堆中发生的情况。我发现使用.NET内存分析器非常有益。

希望对您有所帮助。


1

我建议使用.NET Memory Profiler

.NET Memory Profiler是一个强大的工具,用于查找内存泄漏并优化使用C#、VB.NET或任何其他.NET语言编写的程序的内存使用。

.NET Memory Profiler将帮助您:

  • 查看实时内存和资源信息
  • 通过收集和比较.NET内存的快照轻松识别内存泄漏
  • 查找未正确处理的实例
  • 获取有关非托管资源使用的详细信息
  • 优化内存使用
  • 调查生产代码中的内存问题
  • 执行自动化内存测试
  • 检索有关本机内存的信息

请查看他们的视频教程:

http://memprofiler.com/tutorials/


1

其他人已经提到了IDisposable的重要性,以及在您的代码中需要注意的一些事项。

我想建议一些额外的资源;当学习.NET GC的详细信息以及如何解决.NET应用程序中的内存问题时,我发现以下资源非常宝贵。

CLR via C#由Jeffrey Richter撰写的书籍非常好。仅购买GC和内存章节就值得了。

这个博客(由微软“ASP.NET升级工程师”编写)通常是我寻找WinDbg、SOS的技巧和诀窍以及发现某些类型的内存泄漏的首选来源。Tess甚至设计了.NET调试演示/实验室,将引导您解决常见的内存问题并学会如何识别和解决它们。

Windows调试工具(WinDbg、SOS等)


Tess的博客绝对是这个主题的推荐之选,没错。 - Martin C.

0
什么是最好的删除方法?
注意:以下内容仅适用于包含非托管资源的类型。它对纯托管类型没有帮助。
可能最好的方法是实现并遵循IDisposable模式,并在所有实现它的对象上调用dispose方法。
'using'语句是您最好的朋友。简单地说,它将在实现IDisposable的对象上为您调用dispose。

0

你可以使用像CLR profiler这样的工具,学习如何正确使用它需要一些时间,但毕竟它是免费的。(它帮助我几次找到了内存泄漏)


0

确保对象被删除或在.NET术语中进行垃圾回收的最佳方法是确保对一个对象的所有根引用(可以通过方法和对象跟踪到线程调用堆栈上的第一个方法的引用)都设置为null。

如果有任何根引用指向一个对象,无论它是否实现IDisposable,GC都不能也不会收集该对象。

循环引用不会产生任何惩罚或可能导致内存泄漏,因为GC标记了它已经访问过的对象。在委托或事件处理程序的情况下,可能会忘记从事件中删除对目标方法的引用,因此如果事件被根引用,则包含目标方法的对象无法被收集。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接