1 Windows进程内存管理与GC
进程
在32 位Windows上面,非托管程序的进程都有4G的虚拟地址空间,其中有一半是系统可以访问的,一半是用户可以访问的,由于是虚拟的,只有真正需要用到的时候,才可能会真正提交给该进程。每个进程都是在各自的进程空间中进行资源管理和代码执行,进程本身只是代码执行的环境,它本身并不执行任何代码,它为真正执行代码的线程提供了一个相对独立的资源环境,一般一个进程的崩溃不会影响另外一个进程。进程之间的相互独立性提高了操作系统的稳定性,这样设计是基于每个进程都是不安全的,很可能出现非法的地址访问,内存操作这样影响稳定性的问题。
而说到托管程序,程序集的加载/卸载,内存的管理都是由CLR来进行管理的。托管代码是以程序集为执行单位的。Dot Net的程序集本身是自描述的,且强类型的。这样一来,托管程序的一切对于CLR都是透明的,它明确知道程序集定义的类型,以及需要引用的类型,另外,由于是强类型,它的一切操作的安全性可以得到保证,这样一来,它可以让多一个进程在一起协调工作。
内存
无论托管与非托管进程的内存管理中都会出现 堆 栈 这两个概念。特别是堆,在托管代码中它被称为托管堆 Managed Heap(简称MH),在非托管环境中它是普通的堆 Heap.(H).这两者有一个共同点,需要使用前需要申请,使用后需要释放,一般用于存储比较复杂的类型。那么不同点呢,其实比较多:1)对于H,我们会经常听到一个 “拆了东墙补西墙”的说法,它揭示的特点就是针对普通堆,它的内存是不连续的,“碎片”比较多。可以想象,如果内存非常零碎,进行数据的访问就必须“按图索骥”,显然会影响效率 2)对于MH。它是被CLR直接管理并被优化。MH分为两种,一种是普通MH, 一种是Big MH。Big MH是针对对象的Size大于85k的存储区域。这样做的原因跟CLR的内存管理有着直接关系,说到CLR的内存管理,又不得不说GC,因为是GC真正实现了MH的内存分配和回收。考虑比较简单的情况,一个初始的空的MH.这个初始的MH有一个指针NextObj,指示当前可以申请的内存的初始位置,对于初始的状态,这个NextObj可以认为它在 0 位置,申请一块内存的时候,它将 NextObj的位置 + 需要的内存Size,这样计算它会检测是否已经超出了最大位置,没有超出那么申请内存就表示OK,同时,NextObj就会被移动一个以Size为OffSet的位置,这个NextObj的意义有两点:1)GC知道,我需要需要申请的内存的开始位置,显然提升了申请内存的效率 2)GC可以通过 NextObj + Size 知道当前的内存是否不够了,是否需要进行一次垃圾回收。
垃圾回收
谈及垃圾回收时需要提及一个概念 代 。代是CLR对对象生存期的一个逻辑划分,目前总共有个三代,分别是0,1,2. 新创建的对象处于0代,在经过0代垃圾回收后,它会自动升级为1代,甚至2代。这样分是基于一个考虑,新创建的对象的生存期比之前创建的对象的生存期要短。垃圾回收的过程分为两步:标记,“压缩”。标记的过程就是确认对象是否有被引用,如果没有就可以被回收;回收完成之后,进行内存的压缩,所谓的压缩,就是“碎片整理”,将有被引用的对象移动到一起,那么经过压缩之后,堆看起来就分为两块,有被引用的,没有被引用的区域,这个也是MH的优点,内存碎片相对少。当然,移动的过程意味着之前的对象的引用也需要同步,如果需要调整的地方很多的话,也就有相当效率损耗。
有一点很明确,垃圾回收只存在于两点:用户手工调用GC.COLLECT,或者创建对象时NextObj + New Instance Size 已经超出堆的边界。GC的实现比较复杂,一般情况下没有必要手工的调用GC.Collect,除非遇到操作完一个大的对象,及时释放在效率上更优,否则,一般情况下垃圾回收的时机还是交给系统自动处理比较好。
垃圾回收与内存泄漏
有了垃圾回收,似乎永远也不会出现内存泄漏。其实,这个只是一般情况,也是绝大多数情况,有以下情况需要特别注意:
1) 普通的异步 BeginXXX, EndXXX
由于BeginXXX创建的其他辅助资源需要等待EndXXX时释放,因此,必须保证BeginXXX, EndXXX成对出现
2) 非托管资源 最常见的就是操作本地文件,需要记得关闭。
处理非托管资源的比较
终结方法
终结方法 Finalizer,在非托管程序里面,这个也被叫做析构函数,叫法其实无所谓,但是需要知道它的特点:
1) Finalizer总是有GC调用的,但是GC调用的时机在前面也已经说过了,所以,一般情况下,用户应该不知道总结方法具体什么时候被调用,因此,肯定不能在终结方法里面放置一些跟其他代码有特定逻辑顺序的代码
2)终结方法一定会被执行。
IDispose接口
IDispose接口中定义了一个Dispose方法,任何类型只要实现了该接口,就可以主动调用Dispose()方法及时的释放托管资源,同时,它可以利用Using关键字,保证该方法一定会被得到执行,这个类似于try finally end 结构,但是显然代码简单多了。
正确实现IDispose接口
可以看到了IDispose提供了调用控制,对于Finalizer,它又缺乏必然会被调用特点,那么实现这个IDispose的时候可以结合这两点进行完备的内存释放。
主要有以下几点:
1)不重复释放 定义一个私有的Boolean变量进行释放标记
2) 增加一个Dispose(Boolean i_Disposing) 重载版本,其中I_Disposing指示了调用者,如果是调用IDispose.Dispose方法,那么就可以释放托管资源,和非托管资源;而通过GC调用Finalizer,那么此时托管资源的状态已经处于未知状态,很可能已经被处理过了,那么就不应该再尝试释放托管资源了。
3) Dispose() IDispose接口的Dispose方法 自然调用 Dispose(true), 同时需要调用GC.SuppressCollect提示GC不需要调用该对象的终结方法
4) 终结方法 方法中直接调用 Dispose(false) 即可。
private Boolean m_IsDisposed;
protected Boolean IsDisposed {
get { return m_IsDisposed; }
}
public void Dispose()
{
Dispose(true);
//Rainy: Stop GC calling Finalize.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(
Boolean i_Disposing
)
{
if (m_IsDisposed == false)
{
if (i_Disposing == true)
{
//Free Managed Resource
}
//Free All UnManaged Resource
m_IsDisposed = true;
}
}
~RainyDisposableBase ()
{
//It only frees the unmanaged resource
Dispose(false);
}
}