C#内存管理机制及WP内存泄漏定位方法 一、C#的内存管理机制 .托管资源与非托管资源 什么是托管资源?托管资源通俗的理解就是,把资源交给.net去管理,这些资源主要是数据,比如我们的各种对象,这些对象的回收都由.net来处理。非托管资源则是.net无法进行管理的的资源,必须在程序中显示的进行释放,比如文件、网络连接等。 2.C#的内存区域 在C#中,内存大致分成3个区,分别是堆、栈、静态/常量存储区。 a.静态存储区,Static变量(值类型或者引用类型的指针)及常量存储的区域。 b.栈。 c.堆,堆又分为SOH堆(SmallObjectHeap,也叫GC堆)和LOH(LargeObjectHeap)堆,小于85KB的对象都在SOH堆中进行管理,否则放在LOH堆。LOH堆的内存分配和管理和C语言是很类似的,后面会讲到。 3.SOH堆的内存管理机制-标记和压缩算法。 SOH堆的管理方式可以说是C#语言最大的特征之一,它的职责为回收垃圾并保持堆的空闲空间和已用空间连续。 SOH堆采用标记压缩算法来管理内存,算法分为标记和压缩两个阶段: a.标记并清除:GC先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的。 b.压缩阶段:对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎片整理。执行完后,由于对象被移动了,还要进行一个指针修复的操作,将所有被移动对象的指针修改定位到移动后的位置。 那么GC是怎么确定哪些对象是不可以被回收的?GC从所有的根对象出发开始搜索遍历,将所有能够访问到的对象标记为可到达。其他的对象则为不可到达。根对象包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalizationqueue,后面会讲到)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象。 这种清除不可到达对象的方式,相比引用计数法,可以彻底根除循环引用造成的内存泄漏。 程序运行的时候对象这么多,对全部内存进行GC显然是不划算的。C#这里引入了分代算法,按代来回收,减少内存块移动的次数,依据主要是统计学基础。分代算法的假设前提条件: a.大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长; b.对部分内存进行回收比基于全部内存的回收操作要快; c.新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPUcache的命中率,.NET将heap分成3个代龄区域:Gen0、Gen、Gen2; 当Gen0达到内存阈值,则触发0代GC,Gen0中幸存的对象进入Gen,代GC后,Gen中幸存的对象进入Gen2。代和2代GC被调的很少。这就意味着Gen2的对象会存在比较长的时间。C#提供了GC的接口,那我们是否应该代替平台主动调用GC呢?从这里可以看到,答案是:最好不要主动调用GC。因为主动调用GC会提前把Gen0中的对象送到Gen2,导致这些对象存在更长的时间。 可以看到SOH的已用空间和空闲空间都是连续的,这样有两个好处:一是在请求一块内存的时候效率很高,只要保留一个空闲内存起始位置,每次都从起始位置分配就可以了,这比C语言的链表管理空闲内存块要快很多。二是不存在内存碎片的问题。 4.LOH堆的内存管理。 由于大对象(字节)一般来说都是会存在较长时间,且大块内存的移动非常耗时,所以对于大对象的管理,并没有采用标记-压缩算法,而是把标记为不可达的对象直接删除并清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内存泄漏的定位。 .通过对构造函数和析构函数的调用次数来统计存活对象的个数。 用一个静态变量来记录这个类当前存活的数量,在需要监控的类的基类的构造函数里计数+,在析构函数里计数-。代码如下: 同理,也可以用一个静态的mapTypeName,InstanceCount来记录每一个类的对象数量。只要在关键类的基类的构造函数和析构函数里加代码就可以了。 2.使用Weakrefrence来监控对象的存活。 如果想看某一个对象什么时候释放,C#提供了一个弱引用Weakrefrence,GC搜索可到达对象的时候会忽略Weakrefrence指向的对象,使用方法如下: 3.在WP白癜风哪家医院好北京看白癜风效果最好专科医院
|