避坑指南:可能会导致.NET内存泄露的8种行为

news/2024/12/25 21:28:52/文章来源:https://www.cnblogs.com/kitthe/p/18631436

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

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

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

定义.NET中的内存泄漏

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

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

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

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

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

1.订阅Events

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

代码语言:javascript
复制
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.在匿名方法中捕获类成员

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

这里是一个例子:

代码语言:javascript
复制
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的实例。

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

代码语言:javascript
复制
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对象的托管对象(内存回收将通过引用计数来完成)。

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

代码语言:javascript
复制
public class MyClass
{static List<MyClass> _instances = new List<MyClass>();public MyClass(){_instances.Add(this);}
}

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

4.缓存功能

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

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

代码语言:javascript
复制
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)的强引用,从而导致内存泄漏。

这里是一个例子:

代码语言:javascript
复制
<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将永远留在内存中:

代码语言:javascript
复制
public class MyViewModel
{public string _someText = "memory leak";public string SomeText{get { return _someText; }set{_someText = value;}}
}

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

代码语言:javascript
复制
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。考虑以下代码:

代码语言:javascript
复制
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.没有回收非托管内存

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

这里有一个简单的例子。

代码语言:javascript
复制
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方法,以释放所有非托管资源,如下所示:

代码语言:javascript
复制
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语句:

代码语言:javascript
复制
using (var instance = new MyClass())
{// ...
}

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

代码语言:javascript
复制
MyClass instance = new MyClass();;
try
{// ...
}
finally
{if (instance != null)((IDisposable)instance).Dispose();
}

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

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

代码语言:javascript
复制
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》,以获取有关此内容的更多信息。

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

 

转载:https://cloud.tencent.com/developer/article/2409069

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/858849.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

打印三角形金字塔 、debug、java的方法、命令行传参、可变参数20241225

打印三角形金字塔 debug20241225package com.pangHuHuStudyJava.struct; public class Print_Tran {public static void main(String[] args) {for (int j = 0; j < 5; j++) {for (int r = 5; r > j; r--) {System.out.print( );}for (int s = 0; s < ((2*j)+1); s++…

OpenAI o3模型震撼发布:编程界的革命性突破,程序员的未来将何去何从?

当人工智能踏足编程领域,生产力的提升让人瞠目结舌。就在近日,OpenAI 发布了全新的 o3模型,其强大的代码生成能力和上下文理解能力,将编程带入了一个全新的时代。是机遇还是挑战?程序员们将如何面对这场技术风暴?o3模型究竟有何与众不同之处?它的发布会对程序员和整个软…

[Java/压缩] Java读取Parquet文件

序:契机生产环境有设备出重大事故,又因一关键功能无法使用,亟需将生产环境的原始MQTT报文(以 parquet 文件格式 + zstd 压缩格式 落盘)DOWN到本地,读取并解析。本文聚焦在 本地电脑,用 java 读取 parquet 文件相当多网络文档的读取代码无法正常运行,有必要记录一二,后续…

莫队从入门到人门

普通莫队 详介(P2709 小B的询问) 普通莫队处理问题的前提是问题可以离线,多次区间查询,\(O(n\sqrt m)\) 能过。 我们以 P2709 小B的询问 为例,假设当前区间为 \([l,r]\),答案为 \(ans\),那么 \(r\) 右移一位时,新加入一个数 \(x\),我们只要把 \(ans\) 加上 \(x\) 的贡…

nacos安装注意事项

一年多没玩了,都快忘了,最新版本已经2.3.x了 3.0也快问世了 Linux/Unix/Mac 单机启动命令: sh startup.sh -m standalone Windows startup.cmd -m standalone如果直接未启动就是集群模式,但是要注意nacos.properties里面配置集群信息本文来自博客园,作者:余生请多指教ANT…

PWN系列-2.27版本利用setcontext实现orw

PWN系列-2.27版本利用setcontext实现orw 知识 开启沙箱之后,我们就只能用orw的方式来得到flag。 这篇博客主要讲通过劫持__free_hook或者__malloc_hook利用setcontext在libc或者heap上执行rop或者shellcode。 在free堆块的时候,rdi会指向堆块,在检测到__free_hook有值的情况…

shell语法保姆级教程

Shell脚本 建立一个sh脚本 touch 1.sh (新建脚本文件)vi 1.sh(编写文件内容)按 i 可以写入内容,按esc :wq退出并保存解释 1、创建脚本文件 2、脚本文件中第一行为指定脚本编译器:# !/bin/bash 最终调用的都是dash执行shell脚本命令: 1、./1.sh难道我们必须要修改权限才能执…

从0开始学uniapp——认识HBuilderX

为什么使用uniapp:可以多端运行,写好了这一套可以用在h5,安卓程序,小程序多端,很方便。1.百度搜HBuilderX,使用该编译器学习uniapp 2.新建一个默认项目 pages——用于存放页面,这里都是.vue后缀的页面, pages.json——用于存放路由pages数组里按例子添加即可,HBuilder…

Java中SPI机制原理解析

本文介绍了Java中SPI机制实现的大概原理以及SPI机制在常见的框架如JDBC的Driver加载,SLF4J日志门面实现中的使用。使用SPI机制前后的代码变化加载MySQL对JDBC的Driver接口实现 在未使用SPI机制之前,使用JDBC操作数据库的时候,一般会写如下的代码:// 通过这行代码手动加载My…

Transformers 框架 Pipeline 任务详解(六):填充蒙版(fill-mask)

本文介绍了Hugging Face Transformers框架中的fill-mask任务,涵盖其作用、应用场景如机器翻译和文本补全,以及配置方法。通过Python代码示例展示了如何使用预训练模型自动下载或本地加载来创建Pipeline并执行填空任务。此外,还提供了利用Gradio构建WebUI界面的指南,使用户能…

阿里发布多模态推理模型 QVQ-72B,视觉、语言能力双提升;OpenAI 正在研发人形机器人丨 RTE 开发者日报

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑…

python多进程,通过内存共享来通信,使用进程锁来防止数据问题

代码:import multiprocessing import time 使用锁和multiprocessing.Value,multiprocessing.Array,multiprocessing.Manager().listdef worker1(shared_number1, lock):for _ in range(10):with lock:shared_number1.value += 1def worker2(shared_array1, lock):for i in…