原文连接: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 point
if (!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 MB
private 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》,以获取有关此内容的更多信息。

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

https://zhuanlan.zhihu.com/p/141032986

可能会导致.NET内存泄露的8种行为的更多相关文章

  1. Andorid 内存溢出与内存泄露,几种常见导致内存泄露的写法

    内存泄露,大部分是因为程序的逻辑不严谨,但是又可以跑通顺,然后导致的,内存溢出不会报错,如果不看日志信息是并不知道有泄露的.但是如果一直泄露,然后最终导致的内存溢出,仍然会使程序挂掉.内存溢出大部分是 ...

  2. js内存泄露的几种情况

    想解决内存泄露问题,必须知道什么是内存泄露,什么情况下出现内存泄露,才能在遇到问题时,逐个排除.这里只讨论那些不经意间的内存泄露. 一.什么是内存泄露 内存泄露是指一块被分配的内存既不能使用,又不能回 ...

  3. dotnet 6 在 Win7 系统证书链错误导致 HttpWebRequest 内存泄露

    本文记录我将应用迁移到 dotnet 6 之后,在 Win7 系统上,因为使用 HttpWebRequest 访问一个本地服务,此本地服务开启 https 且证书链在此 Win7 系统上错误,导致应用 ...

  4. java 内存泄露的几种情况

    内存泄漏定义(memory leak):一个不再被程序使用的对象或变量还在内存中占有存储空间. 一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出.内存溢出 out of memory ...

  5. 深度:ARC会导致的内存泄露

    iOS提供了ARC功能,很大程度上简化了内存管理的代码. 但使用ARC并不代表了不会发生内存泄露,使用不当照样会发生内存泄露. 下面列举两种内存泄露的情况. 1,循环参照 A有个属性参照B,B有个属性 ...

  6. Java中的内存泄露的几种可能

    Java内存泄漏引起的原因: 内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏. 长生命周期的对象持有短生命周期对象的引用就很可能发 ...

  7. java中内存泄露有几种?如何分析泄露原因

    一.Java内存回收机制 不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址.Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Hea ...

  8. JavaScript之详述闭包导致的内存泄露

    一.内存泄露 1. 定义:一块被分配的内存既不能使用,也不能回收.从而影响性能,甚至导致程序崩溃. 2. 起因:JavaScript的垃圾自动回收机制会按一定的策略找出那些不再继续使用的变量,释放其占 ...

  9. js内存泄露的几种情况详细探讨

    内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束.在C++中,因为是手动管理内存,内存泄露是经常出现的事情.而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使 ...

随机推荐

  1. redis: Jedis API(十四)

    1.Key操作 package com.kuang; import redis.clients.jedis.Jedis; import java.util.Set; public class Test ...

  2. 几个可以提高工作效率的Python内置小工具

    在这篇文章里,我们将会介绍4个Python解释器自身提供的小工具.这些小工具在笔者的日常工作中经常用到,减少了各种时间的浪费,然而,却很容易被大家忽略.每当有新来的同事看到我这么使用时,都忍不住感叹, ...

  3. 干货最实用的 Python 多线程代码框架

    前言 很多地方都要用到多线程,这是我经常用的多线程代码,放在博客园记录下. 代码 from multiprocessing.pool import ThreadPool thread = 10 ite ...

  4. AJAX教程——检视阅读

    AJAX教程--检视阅读 参考 AJAX 教程--菜鸟 AJAX 教程--w3cschool AJAX 教程--w3school.cn AJAX 教程--易百 AJAX = Asynchronous ...

  5. VideoView--简单获取进度条的方法

    使用MediaController类就可以简单的把视频中的进度条加进去 实例: 现在布局哪里放一个VideoView,然后: videoView = (VideoView) findViewById( ...

  6. 小程序里json字符串转json对象需注意的地方

    一.JSON字符串转换为JSON对象 要使用上面的str1,必须使用下面的方法先转化为JSON对象: //由JSON字符串转换为JSON对象 var obj = eval('(' + str + ') ...

  7. 【抓包工具】tcpdump

    tcpdump - dump traffic on a network 根据使用者的定义对网络上的数据包进行截获的包分析工具. tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析.它支 ...

  8. JS实现元素的全屏、退出全屏功能

     在实际开发中,我们很可能需要实现某一元素的全屏和退出全屏功能,如canvas.所幸的是,js提供了相关api用来处理这一问题,只需简单的调用requestFullScreen.exitFullScr ...

  9. 【蜕变之路】第20天 UUID和时间戳的生成 (2019年3月10日)

    Hello,大家好!我是程序员阿飞!今天主要学习的内容是:字符串UUID的随机生成和时间戳的随机生成.好了,直接进入正题. 1.UUID的随机生成 /*          * uuid的随机生成方式 ...

  10. Ubuntu上安装配置Java环境

    参考文献:在Ubuntu 14.04中安装JDK 方法一: @ 安装 1. 添加PPA repository系统 PPA repository介绍 $sudo add-apt-repository p ...