原文连接:https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
作者 Michael Shpilt。授权翻译,转载请保留原文链接。

任何有经验的.NET开发人员都知道,即使.NET应用程序具有垃圾回收器,内存泄漏始终会发生。 并不是说垃圾回收器有bug,而是我们有多种方法可以(轻松地)导致托管语言的内存泄漏。

内存泄漏是一个偷偷摸摸的坏家伙。 很长时间以来,它们很容易被忽视,而它们也会慢慢破坏应用程序。 随着内存泄漏,你的内存消耗会增加,从而导致GC压力和性能问题。 最终,程序将在发生内存不足异常时崩溃。

在本文中,我们将介绍.NET程序中内存泄漏的最常见原因。 所有示例均使用C#,但它们与其他语言也相关。

定义.NET中的内存泄漏

在垃圾回收的环境中,“内存泄漏”这个术语有点违反直觉。 当有一个垃圾回收器(GC)负责收集所有东西时,我的内存怎么会泄漏呢?

这里有两个核心原因。 第一个核心原因是你的对象仍被引用但实际上却未被使用。 由于它们被引用,因此GC将不会收集它们,这样它们将永久保存并占用内存。 例如,当你注册了事件但从不注销时,就有可能会发生这种情况。 我们称其为托管内存泄漏。

第二个原因是当你以某种方式分配非托管内存(没有垃圾回收)并且不释放它们。 这并不难做到。 .NET本身有很多会分配非托管内存的类。 几乎所有涉及流、图形、文件系统或网络调用的操作都会在背后分配这些非托管内存。 通常这些类会实现 Dispose 方法,以释放内存。 你自己也可以使用特殊的.NET类(如Marshal)或PInvoke轻松地分配非托管内存。

许多人都认为托管内存泄漏根本不是内存泄漏,因为它们仍然被引用,并且理论上可以被回收。 这是一个定义问题,我的观点是它们确实是内存泄漏。 它们拥有无法分配给另一个实例的内存,最终将导致内存不足的异常。 对于本文,我会将托管内存泄漏和非托管内存泄漏都归为内存泄漏。

以下是最常见的8种内存泄露的情况。 前6个是托管内存泄漏,后2个是非托管内存泄漏:

1.订阅Events

.NET中的Events因导致内存泄漏而臭名昭著。 原因很简单:订阅事件后,该对象将保留对你的类的引用。 除非你使用不捕获类成员的匿名方法。 考虑以下示例:

public class MyClass
{public MyClass(WiFiManager wiFiManager){wiFiManager.WiFiSignalChanged += OnWiFiChanged;}private void OnWiFiChanged(object sender, WifiEventArgs e){// do something}
}

假设wifiManager的寿命超过MyClass,那么你就已经造成了内存泄漏。 wifiManager会引用MyClass的任何实例,并且垃圾回收器永远不会回收它们。

Event确实很危险,我写了整整一篇关于这个话题的文章,名为《5 Techniques to avoid Memory Leaks by Events in C# .NET you should know.》

所以,你可以做什么呢? 在提到的这篇文章中,有几种很好的模式可以防止和Event有关的内存泄漏。 无需详细说明,其中一些是:

  • 注销订阅事件。
  • 使用弱句柄(weak-handler)模式。
  • 如果可能,请使用匿名函数进行订阅,并且不要捕获任何类成员。

2.在匿名方法中捕获类成员

虽然可以很明显地看出事件机制需要引用一个对象,但是引用对象这个事情在匿名方法中捕获类成员时却不明显了。

这里是一个例子:

public class MyClass
{private JobQueue _jobQueue;private int _id;public MyClass(JobQueue jobQueue){_jobQueue = jobQueue;}public void Foo(){_jobQueue.EnqueueJob(() =>{Logger.Log($"Executing job with ID {_id}");// do stuff });}
}

在代码中,类成员_id是在匿名方法中被捕获的,因此该实例也会被引用。 这意味着,尽管JobQueue存在并已经引用了job委托,但它还将引用一个MyClass的实例。

解决方案可能非常简单——分配局部变量:

public class MyClass
{public MyClass(JobQueue jobQueue){_jobQueue = jobQueue;}private JobQueue _jobQueue;private int _id;public void Foo(){var localId = _id;_jobQueue.EnqueueJob(() =>{Logger.Log($"Executing job with ID {localId}");// do stuff });}
}

通过将值分配给局部变量,不会有任何内容被捕获,并且避免了潜在的内存泄漏。

3.静态变量

我知道有些开发人员认为使用静态变量始终是一种不好的做法。 尽管有些极端,但在谈论内存泄漏时的确需要注意它。

让我们考虑一下垃圾收集器的工作原理。 基本思想是GC遍历所有GC Root对象并将其标记为“不可收集”。 然后,GC转到它们引用的所有对象,并将它们也标记为“不可收集”。 最后,GC收集剩下的所有内容。

那么什么会被认为是一个GC Root?

  1. 正在运行的线程的实时堆栈。
  2. 静态变量。
  3. 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)。

这意味着静态变量及其引用的所有内容都不会被垃圾回收。 这里是一个例子:

public class MyClass
{static List<MyClass> _instances = new List<MyClass>();public MyClass(){_instances.Add(this);}
}

如果你出于某种原因而决定编写上述代码,那么任何MyClass的实例将永远留在内存中,从而导致内存泄漏。

4.缓存功能

开发人员喜欢缓存。 如果一个操作能只做一次并且将其结果保存,那么为什么还要做两次呢?

的确如此,但是如果无限期地缓存,最终将耗尽内存。 考虑以下示例:

public class ProfilePicExtractor
{private Dictionary<int, byte[]> PictureCache { get; set; } = new Dictionary<int, byte[]>();public byte[] GetProfilePicByID(int id){// A lock mechanism should be added here, but let's stay on pointif (!PictureCache.ContainsKey(id)){var picture = GetPictureFromDatabase(id);PictureCache[id] = picture;}return PictureCache[id];}private byte[] GetPictureFromDatabase(int id){// ...}
}

这段代码可能会节省一些昂贵的数据库访问时间,但是代价却是使你的内存混乱。

你可以做一些事情来解决这个问题:

  • 删除一段时间未使用的缓存。
  • 限制缓存大小。
  • 使用WeakReference来保存缓存的对象。 这依赖于垃圾收集器来决定何时清除缓存,但这可能不是一个坏主意。 GC会将仍在使用的对象推广到更高的世代,以使它们的保存时间更长。 这意味着经常使用的对象将在缓存中停留更长时间。

5.错误的WPF绑定

WPF绑定实际上可能会导致内存泄漏。 经验法则是始终绑定到DependencyObject或INotifyPropertyChanged对象。 如果你不这样做,WPF将创建从静态变量到绑定源(即ViewModel)的强引用,从而导致内存泄漏。

这里是一个例子:

<UserControl x:Class="WpfApp.MyControl"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><TextBlock Text="{Binding SomeText}"></TextBlock>
</UserControl>

这个View Model将永远留在内存中:

public class MyViewModel
{public string _someText = "memory leak";public string SomeText{get { return _someText; }set{_someText = value;}}
}

而这个View Model不会导致内存泄漏:

public class MyViewModel : INotifyPropertyChanged
{public string _someText = "not a memory leak";public string SomeText{get { return _someText; }set{_someText = value;PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof (SomeText)));}}

是否调用PropertyChanged实际上并不重要,重要的是该类是从INotifyPropertyChanged派生的。 因为这会告诉WPF不要创建强引用。

另一个和WPF有关的内存泄漏问题会发生在绑定到集合时。 如果该集合未实现INotifyCollectionChanged接口,则会发生内存泄漏。 你可以通过使用实现该接口的ObservableCollection来避免此问题。

6.永不终止的线程

我们已经讨论过了GC的工作方式以及GC root。 我提到过实时堆栈会被视为GC root。 实时堆栈包括正在运行的线程中的所有局部变量和调用堆栈的成员。

如果出于某种原因,你要创建一个永远运行的不执行任何操作并且具有对对象引用的线程,那么这将会导致内存泄漏。

这种情况很容易发生的一个例子是使用Timer。考虑以下代码:

public class MyClass
{public MyClass(){Timer timer = new Timer(HandleTick);timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));}private void HandleTick(object state){// do something}

如果你并没有真正的停止这个timer,那么它会在一个单独的线程中运行,并且由于引用了一个MyClass的实例,因此会阻止该实例被收集。

7.没有回收非托管内存

到目前为止,我们仅仅谈论了托管内存,也就是由垃圾收集器管理的内存。 非托管内存是完全不同的问题,你将需要显式地回收内存,而不仅仅是避免不必要的引用。

这里有一个简单的例子。

public class SomeClass
{private IntPtr _buffer;public SomeClass(){_buffer = Marshal.AllocHGlobal(1000);}// do stuff without freeing the buffer memory}

在上述方法中,我们使用了Marshal.AllocHGlobal方法,它分配了非托管内存缓冲区。 在这背后,AllocHGlobal会调用Kernel32.dll中的LocalAlloc函数。 如果没有使用Marshal.FreeHGlobal显式地释放句柄,则该缓冲区内存将被视为占用了进程的内存堆,从而导致内存泄漏。

要解决此类问题,你可以添加一个Dispose方法,以释放所有非托管资源,如下所示:

public class SomeClass : IDisposable
{private IntPtr _buffer;public SomeClass(){_buffer = Marshal.AllocHGlobal(1000);// do stuff without freeing the buffer memory}public void Dispose(){Marshal.FreeHGlobal(_buffer);}
}

由于内存碎片问题,非托管内存泄漏比托管内存泄漏更严重。 垃圾回收器可以移动托管内存,从而为其他对象腾出空间。 但是,非托管内存将永远卡在它的位置。

8.添加了Dispose方法却不调用它

在最后一个示例中,我们添加了Dispose方法以释放所有非托管资源。 这很棒,但是当有人使用了该类却没有调用Dispose时会发生什么呢?

为了避免这种情况,你可以在C#中使用using语句:

using (var instance = new MyClass())
{// ...
}

这适用于实现了IDisposable接口的类,并且编译器会将其转化为下面的形式:

MyClass instance = new MyClass();;
try
{// ...
}
finally
{if (instance != null)((IDisposable)instance).Dispose();
}

这非常有用,因为即使抛出异常,也会调用Dispose。

你可以做的另一件事是利用Dispose Pattern。 下面的示例演示了这种情况:

public class MyClass : IDisposable
{private IntPtr _bufferPtr;public int BUFFER_SIZE = 1024 * 1024; // 1 MBprivate bool _disposed = false;public MyClass(){_bufferPtr =  Marshal.AllocHGlobal(BUFFER_SIZE);}protected virtual void Dispose(bool disposing){if (_disposed)return;if (disposing){// Free any other managed objects here.}// Free any unmanaged objects here.Marshal.FreeHGlobal(_bufferPtr);_disposed = true;}public void Dispose(){Dispose(true);GC.SuppressFinalize(this);}~MyClass(){Dispose(false);}
}

这种模式可确保即使没有调用Dispose,Dispose也将在实例被垃圾回收时被调用。 另一方面,如果调用了Dispose,则finalizer将被抑制(SuppressFinalize)。 抑制finalizer很重要,因为finalizer开销很大并且会导致性能问题。

然而,dispose-pattern不是万无一失的。 如果从未调用Dispose并且由于托管内存泄漏而导致你的类没有被垃圾回收,那么非托管资源也将不会被释放。

总结

知道内存泄漏是如何发生的很重要,但只有这些还不够。 同样重要的是要认识到现有应用程序中存在内存泄漏问题,找到并修复它们。 你可以阅读我的文章《Find, Fix, and Avoid Memory Leaks in C# .NET: 8 Best Practices》,以获取有关此内容的更多信息。

希望你喜欢这篇文章,并祝你编程愉快。

会不会导致内存泄漏_可能会导致.NET内存泄露的8种行为相关推荐

  1. unity如何检测内存泄漏_如何排查Java内存泄漏?看懂这一篇就够用了

    原文:https://www.toptal.com/java/hunting-memory-leaks-in-java 作者:Jose Ferreirade Souza Filho 译者:Emma来源 ...

  2. jstat 内存泄漏_一次Java内存泄漏的排查!要了自己的老命!

    点击上方"Java之间",选择"置顶或者星标" 你关注的就是我关心的! 作者:枕边书 来源:https://zhenbianshu.github.io 一.由来 ...

  3. jni jvm 内存泄漏_解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互--在 Java 程序中调用 native ...

  4. 内存泄漏_内存泄漏–测量频率和严重性

    内存泄漏 这篇文章是我们开放文化的一部分-我们将继续分享日常工作中的见解. 这次,我们窥视了我们价值主张的核心,即–寻找以下问题的答案: Java应用程序中内存泄漏多久发生一次? 内存泄漏有多大? 内 ...

  5. 多线程内存泄漏_内存泄漏的场景和解决办法

    1.非静态内部类会持有外部类的引用,如果非静态内部类的实例是静态的,就会长期的维持着外部类的引用,组织被系统回收,解决办法是使用静态内部类 2.多线程相关的匿名内部类和非静态内部类 匿名内部类同样会持 ...

  6. Java内存泄漏系列--匿名内部类导致内存泄露--原因/解决方案

    原文网址:Java内存泄漏系列--匿名内部类导致内存泄露--原因/解决方案_IT利刃出鞘的博客-CSDN博客 简介 说明 本文用示例介绍匿名内部类会导致内存泄漏的原因及其解决方案. 相关网址 普通内部 ...

  7. java内部类内存泄漏,Android中常见的内存泄漏和解决方案

    什么是内存泄漏? 简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏. 为什么会产生内存泄漏,内存泄漏会导致什么问题? 相比C++需要手动去 ...

  8. c语言内存泄漏怎么测试,C语言内存泄漏检测方法

    内存泄漏是C语言编程中一个很常见的问题,而且由于内存泄漏所导致的问题出现较缓慢,所以不容易觉察,所以写一个简单的程序来检测内存泄漏很有必要. 内存泄漏通常是指堆内存的泄漏,也就是通过malloc.ca ...

  9. valgrind 内存泄漏_应用 AddressSanitizer 发现程序内存错误

    应用 AddressSanitizer 发现程序内存错误 作为 C/ C++ 工程师,在开发过程中会遇到各类问题,最常见便是内存使用问题,比如,越界,泄漏.过去常用的工具是 Valgrind,但使用 ...

最新文章

  1. linux虚拟用户的配置
  2. VS2013的项目转到VS2010需要修改的
  3. 堆内存破坏检测实战--附完整调试过程
  4. linux c之memcpy拷贝结构体到结构体、拷贝字符数组到结构体
  5. from表单iframe原网页嵌入
  6. lnmp环境配置laravel项目白屏问题
  7. java中随机生成汉字
  8. HTML5 响应式网页设计之页面美化(一.响应式布局)
  9. PHP设计模式——建造者模式
  10. oracle建表的方法,oracle建表语句
  11. 清新简约的24小时在线要饭网站源码 全开源版+免费分享
  12. MVCC(多版本并发控制)原理
  13. osg-04-基本几何图元-线宽
  14. 山东理工大学ACM平台题答案关于C语言 1580 闰年
  15. 给十二星座的12封信,句句说中你们的心理要害!
  16. Python练习:炉石传说荣誉室返尘最优策略
  17. SAP ABAP ZBA_R002 查询用户下的角色
  18. 数据结构-----------------------哈希表(最通俗易懂的文章)
  19. Quartus II 13.0波形仿真
  20. Java题目训练——星际密码和数根

热门文章

  1. 视频编解码器 2020-比赛开始!
  2. ElasticSearch搜索语法学习(term,filter,bool,terms,range)
  3. C++编程常见问题—error: passing 'const std::map]' discards qualifiers或pass-by-reference-to-const-map导致的“d
  4. Vue014_ vue  项目的打包与发布
  5. 小米 MySQL 数据实时同步到大数据数仓的架构与实践
  6. springboot的核心
  7. leetcode 446. Arithmetic Slices II - Subsequence | 446. 等差数列划分 II - 子序列(动态规划)
  8. leetcode 371. Sum of Two Integers | 371. 两整数之和(补码运算)
  9. leetcode 557. 反转字符串中的单词 III(Java版)
  10. dart系列之:dart语言中的函数