百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

C#内存管理机制及WP内存泄漏定位方法

bigegpt 2024-08-02 10:46 9 浏览

C#内存管理机制及WP内存泄漏定位方法

一、C#的内存管理机制

1. 托管资源与非托管资源

什么是托管资源?托管资源通俗的理解就是,把资源交给.net去管理,这些资源主要是数据,比如我们的各种对象,这些对象的回收都由.net来处理。非托管资源则是.net无法进行管理的的资源,必须在程序中显示的进行释放,比如文件、网络连接等。


2. C#的内存区域

在C#中,内存大致分成3个区,分别是堆、栈、静态/常量存储区。

a. 静态存储区,Static变量(值类型或者引用类型的指针)及常量存储的区域。

b. 栈。

c. 堆,堆又分为SOH堆(Small Object Heap,也叫GC堆)和LOH(Large Object Heap)堆,小于85KB的对象都在SOH堆中进行管理,否则放在LOH堆。LOH堆的内存分配和管理和C语言是很类似的,后面会讲到。


3. SOH堆的内存管理机制-标记和压缩算法。

SOH堆的管理方式可以说是C#语言最大的特征之一,它的职责为回收垃圾并保持堆的空闲空间和已用空间连续。

SOH堆采用标记压缩算法来管理内存,算法分为标记和压缩两个阶段:

a. 标记并清除:GC先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的。


b. 压缩阶段:对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎片整理。执行完后,由于对象被移动了,还要进行一个指针修复的操作,将所有被移动对象的指针修改定位到移动后的位置。


那么GC是怎么确定哪些对象是不可以被回收的?GC从所有的根对象出发开始搜索遍历,将所有能够访问到的对象标记为可到达。其他的对象则为不可到达。根对象包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue,后面会讲到)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象。

这种清除不可到达对象的方式,相比引用计数法,可以彻底根除循环引用造成的内存泄漏



程序运行的时候对象这么多,对全部内存进行GC显然是不划算的。C#这里引入了分代算法,按代来回收,减少内存块移动的次数,依据主要是统计学基础。分代算法的假设前提条件:

a. 大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长;

b. 对部分内存进行回收比基于全部内存的回收操作要快;

c. 新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率,.NET将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2;

当Gen0达到内存阈值,则触发0代GC,Gen0中幸存的对象进入Gen1,1代GC后,Gen1中幸存的对象进入Gen2。1代和2代GC被调的很少。这就意味着Gen2的对象会存在比较长的时间。C#提供了GC的接口,那我们是否应该代替平台主动调用GC呢?从这里可以看到,答案是:最好不要主动调用GC。因为主动调用GC会提前把Gen0中的对象送到Gen2,导致这些对象存在更长的时间。

可以看到SOH的已用空间和空闲空间都是连续的,这样有两个好处:一是在请求一块内存的时候效率很高,只要保留一个空闲内存起始位置,每次都从起始位置分配就可以了,这比C语言的链表管理空闲内存块要快很多。二是不存在内存碎片的问题。


4. LOH堆的内存管理。

由于大对象(>85000字节)一般来说都是会存在较长时间,且大块内存的移动非常耗时,所以对于大对象的管理,并没有采用标记-压缩算法,而是把标记为不可达的对象直接删除并清0内存,然后像操作系统一样使用一个链表链来管理空闲内存。当请求一块内存时,遍历空闲内存链表找到合适大小的内存块来满足请求。LOH的回收时机是在SOH中二代GC的时候。

所以大对象的分配会更慢,并且会产生内存碎片。


5. 析构函数(在C#中叫做Finalizer)

在GC过程中,遇到有析构函数的对象,会怎么处理?因为析构函数的复杂度是未知的,有可能非常耗时,所以在GC的过程中调用析构函数是不明智的。于是遇到有析构函数的对象,把这些对象放到一个待析构队列。会有一个低优先级的线程去执行这些对象的析构函数。为了兼容程序员在析构函数里激活对象,比如在析构函数里把this赋值给一个静态变量导致对象又变成可到达了,GC在执行完析构函数之后再决定是否要从内存里删除这个对象。可见,除非是需要在析构函数中释放非托管资源,其他任何情况下都不应该使用析构函数,因为析构函数会导致对象的内存被延后释放并带来额外开销。


6. 非托管资源的处理

非托管资源,诸如文件、网络Socket、摄像头等资源GC是没有办法释放的。我们可以用一个代理对象来封装一个非托管资源,并在析构函数里进行释放非托管资源,这样可以确保非托管资源不泄漏。

一旦要使用析构函数,就会加大GC的负担。那么如何能保障非托管资源不泄露,又有不错的性能呢?C#提供了IDisposable接口和GC.SuppressFinalize(功能是让GC忽略对象的析构函数),所以处理非托管资源的正确方式应该是这样:

a. 继承IDisposable接口;

b. 实现Dispose()方法,在其中释放托管资源和非托管资源,并调用GC.SuppressFinalize将对象本身从垃圾回收器中移除(垃圾回收器不在回收此资源);

c. 实现类析构函数,在其中释放非托管资源。

到目前看起来,好像IDisposable没有什么特殊,似乎随便自己写一个函数也能满足相同的功能。但其实C#对IDisposable的子类是有相应的语言支持的。比如使用using块的时候,编译器会自动增加调用对象的Dispose方法,并且确保异常发生的情况下,Dispose接口也会被调用到。比如下面这个代码:

会被编译器翻译成:


7. 值类型和引用类型

C#几乎所有的类型都继承自Object,当你用class声明一个没有基类的类的时候,是隐式继承自Object的,而Object还有一个特殊的子类ValueType,所有用Struct关键字声明的类型都隐式继承自ValueType,ValueType的子类就是值类型。所以区分值类型和引用类型的方式就是,看它是用Struct声明还是用Class声明。可以看到int、long这些基础类型都是用struct声明的。

引用类型通过new关键字创建,对象都是存储在堆里的,值类型则不一样,值类型的对象在函数中声明时,即使是通过new关键字创建,也是在栈中分配。

引用类型的特征就是永远是指针,永远按指针传递,而值类型则永远按值传递,区别可以看下面的代码:

那么问题来了,引用类型值类型到底哪家强?我认为大部分情况下都应该使用引用类型,因为共享同一个copy可以减少内存的占用,在参数传递时只传递指针也要更高效,但下面几种情况我认为应该考虑使用值类型:

a. 如果有大量生命周期短的小对象,比如在一些循环中需要反复创建和销毁的小型数据结构,那么应该使用值类型,因为值类型在栈上创建非常快,并且不会给GC带来负担。

b. 如果需要对数据进行”拍照”来快速获取并保留数据的状态,也可以用值类型。比如Datetime,每次获取都是获得一个Copy,可以及时的保存当前的时间。

c. 数据实在太小,又不需要共享一个copy的情况,比如Point,Size这种结构。

如果既需要像引用类型一样减少重复内容,又需要像值类型一样确保copy不会被其他地方修改。那么C#的string类就是最好的例子。个人感觉C#string的好用程度秒杀std::string。原因如下:

a. C#string是一个引用类型,所以你在传值时不必担心会重复创建内存。这点std::string就经常被迫需要复制一份新的std::string出来从而造成重复的内存分配和复制,且C语言的内存分配还很低效。

b. C#string不提供任何对已存在string修改的接口,所有的接口都是返回一个新的C#string,比如C#string.replace(),其实是新创建了一个string返回。这样保证了共享一个对象的时候不用担心这个对象从其他地方被修改,这又是值类型的优点。

c. 提供StringBuilder类来处理构建C#string的过程,不会引起C#String构建过程中+=这种操作造成大量小对象。


8. 小结

a. 在堆中分配内存(<85KB),C#是非常高效的,比C要快的多。

b. 相比IOS平台使用的引用计数的方式来管理内存,效率要高一些,但是有循环引用的陷阱。

c. 最好不要主动调用GC.Collect(),因为这会提前把一些对象移到第二代堆里。导致这些对象的回收变慢。

d. 尽量避免使用超大对象(>85KB),因为这类对象回收频率很低,分配很慢,还会造成内存碎片。

e. 没有非托管资源的时候不要使用析构函数。

f. 处理非托管资源,要遵循规范使用IDisposable接口、GC.SuppressFinalize、以及析构函数。

g. 使用非托管资源,最好使用using块。

h. 必要的情况下,可以考虑使用值类型。



二、发现内存泄漏

微软提供了工具可以查看程序运行过程中各种对象的数量,但是这个工具非高内存电脑跑不起来,跑一次需要的时间也很久。这套工具royle比较熟悉,我研究的较少,就不在这里讨论了。

WP中占内存最大的还是UI,所以这里主要讨论的也是UI内存泄漏的定位。

1. 通过对构造函数和析构函数的调用次数来统计存活对象的个数。

用一个静态变量来记录这个类当前存活的数量,在需要监控的类的基类的构造函数里计数+1,在析构函数里计数-1。代码如下:

同理,也可以用一个静态的map<TypeName, InstanceCount>来记录每一个类的对象数量。只要在关键类的基类的构造函数和析构函数里加代码就可以了。


2. 使用Weakrefrence来监控对象的存活。

如果想看某一个对象什么时候释放,C#提供了一个弱引用Weakrefrence,GC搜索可到达对象的时候会忽略Weakrefrence指向的对象,使用方法如下:


3. 在WP微信中是如何发现内存泄漏的。

WP微信使用对象计数的方式来初步发现内存泄漏,如果已经离开一个页面,但这个页面仍然有存活的实例,那么就说明这个页面发生泄漏了。可见要发现UI的内存泄漏,还是很容易的。



三、如何定位泄漏的原因。

1. WP UI树的结构

发现UI有内存泄漏后,往往还是很难得知具体的泄漏点,这和WP UI结构有关系。众所周知,WP的UI结构是一颗树,但从内存引用关系的角度来看,在UI树上,任意相连的两个节点之间的连接并不是单向的,而是双向的,举个例子:一个Panel通过Children容器引用了所有的子元素,而每一个子元素又通过Parent属性引用其父控件。这样导致的结果就是从任一个节点出发去遍历内存,都能遍历完整个UI树,这意味着WP的UI结构在内存的视角上其实是一个强连通图,任何一个元素的泄漏都会引起整个Page所有元素的泄漏。这样一来,我们就很难知道具体是哪个控件引起的泄漏,因为真凶隐藏在人民群众的汪洋大海中了。


2. 拆散UI树

前面提到UI树中元素的引用关系是一个强连通图,所以只要找到办法将这个图破坏掉,让真凶失去群众基础,就可以逼出真凶了。这里直接上代码:

遍历整个UI树,将所有的UI元素的子元素清空。


3. 将UI内部的代码引用置为NULL

完成上一步后,其实还没有完全拆散UI元素之间的引用关系。原因在于我们在写xaml时会用x:Name为很多元素取名字。

xaml会被IDE处理成这样的代码:

可以看到这里Page里面会有很多指针引用了子元素。但经过观察,发现_contentLoaded这个变量永远都会在所有的x:Name自动生成的变量后面。于是利用反射,可以有一个猥琐的方法来实现将这些指针置为NULL,详见下面代码:

至此,UI树中元素大部分的引用关系已经被解除了,剩下的引用关系主要是UI元素之间事件的监听以及业务本身逻辑所导致的引用。



4. 使用WeakRefrence来最终定位泄漏点

如果已经确定一个页面存在泄漏,那么可以在这个页面退出的时候,将页面所有的元素通过上面说的方式拆散并放入一个WeakRefrence数组中,过10秒左右再查看这个WeakRfrence数组中哪些对象是存活的,存活的对象就是泄漏点了。这10秒内可以适当做几次GC.Collect()


5. 查找泄漏原因

a. 泄漏的原因主要还是监听了事件中心的事件。所以看看该类代码中注册事件监听和反注册监听是否配对,在代码中搜索+=。

b. 其他被引用导致的泄漏,一般可以在泄漏的类中搜索this指针,看this指针是否有被添加到一些静态变量中。


6. 小结

查找内存泄漏的步骤分为三部:

a. 发现泄漏(存活对象计数)

b. 缩小观察范围(尽量解除元素之间的引用关系)

c. 对可疑泄漏类查找泄漏原因(在代码中搜索this指针及+=回调)

可以把a和b中的逻辑分别封装成单独的工具类。




四、一次实际的寻找内存泄漏的例子

WP微信中已经将发现泄漏和定位泄漏的逻辑封装成了工具类,并有相应的UI展示,下面是一次实际的使用案例。


1. 发现泄漏

装上WP微信Debug版本,使用一段时间后,查看计数的UI个数:

可以看到OfficialAccountSessionList(公众号会话列表)这个页面存在3个实例没有释放,于是发现一个内存泄漏的页面。


2. 定位泄漏点

打开提示泄漏定位功能,再次进入公众账号会话列表,然后退出,静等10秒左右。当前泄漏的控件为:

可以看到:有三个泄漏类型:页面,MMListBox,和SessionListItem。

这个三个类型通过回调以及数据互相有引用关系,所以同时泄漏了。


3. 分析泄漏原因

其中MMListBox是一个公用控件,不会是泄漏的源头,排除在外。

SessionListItem是列表项,没有数据的时候就不会有列表项,所以排除法试一下没有数据的情况,进入公众号会话列表看看还会不会泄漏。结果是,没有数据,这个页面就不会泄漏了。所以可以认定SessionListItem是泄漏点。

查看SessionListItem的代码,搜索this指针的传递,发现this指针被多处静态集合引用,挨个排除找到最后引起泄漏的原因为this指针被传入到一个静态集合里,却没有在合适的时机被解除引用。

作者:andrewu

来源:微信公众号:微信客户端技术团队

出处:https://mp.weixin.qq.com/s/wc66ZyT_8nBc8M8a6kHOzA

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...