c#内存管理
在.net中内存资源可以分为托管资源和非托管资源。托管资源受CLR的管理,非托管资源则不受。
1、托管资源
托管资源分别存放在“栈”和“托管堆”。
“值类型的实例作为局部变量时”和“引用类型的实例的引用”会存放在栈上。
“值类型的实例作为类的成员时”和“引用类型的实例”会存放在堆上。
当作为方法的参数时,值类型的变量直接拷贝一份, 引用类型拷贝一个指向同一对象的引用副本。
1.1、栈
栈又称线程栈,空间比较小,是一个先进后出的结构,内存是由地址高往低填充。
栈有一个指针,指向栈的下一个可用空间,当有变量入栈时,指针往低地址移动,当变量出栈时,指针回到高地址。
public static void Main() { int n = 1; char c = 'a'; }
以上述代码为例,假设地址从2000开始。代码从Main开始执行,首先进入的是变量n,因为是整型所以占用4个字节,指针下移四个字节,变量n的值1存的地址为1997~2000。
下一行代码,为字符型变量c分配两个字节的空间,指针下移两个字节,变量c的值‘a’存的地址为1995~1996。
最后执行到Main函数的右括号,执行结束,删除num和c的值。指针先上移2个字节,删除c的值,再上移4个字节,删除num的值。
由于栈的内存是连续的,且只能从栈顶拿出或进入,所以不需要进行GC。
1.2、堆
堆是托管堆的简称,是由CLR进行管理的。它是程序运行时,CLR申请的一块内存,是属于进程内存空间的一部分。
这块空间可以划分为:
1.GC堆:分为0,1,2代三块区域,代越大空间越大,其中大对象堆(large object heap)是2代堆的一部分。
2.几个程序域:上面有各自的加载堆(loader heap),加载堆上存了方法表(Method Table),上面存储了静态字段、所有的函数、基类型、实现的接口等等。加载堆不受GC影响,其生命周期为创建到程序域卸载。
GC的三代回收机制:新创建的对象是第0代,当第0代满时,便触发垃圾回收,没有被回收的则会变成第1代。以此类推,当1代回收时,留下来的变成了2代。
大对象堆:超过85KB的实例会存放在大对象堆上。
堆中实例的布局如下:
同步块索引(synchronization block index):同步块索引,又被叫做对象头指针,位于对象地址的-4到0字节(以32位机型为例)。
同步块索引在线程同步中用来判断对象是被使用还是闲置。
当有线程要对某个对象操作时,检查其同步块索引的值,若为表示闲置的值,便进行操作。
然后在CLR的同步块数组中增加一块新的同步块,把此块的索引值写入同步块的索引值里,表示其被占用。
线程用完后,便把索引值改回表示闲置的值。
方法表指针:方法表指针指向类型对象。
类型对象是加载程序集时,在加载堆中创建的。
类型对象存储了静态字段和方法表,被所有该类型的实例共享。
方法表包括所有函数,包括静态函数和非静态函数。
2、非托管资源
非托管资源是不受CLR管理的资源,最常见的非托管资源是包装操作系统资源的对象,例如文件、网络连接、数据库连接等。
C#提供Object.Finalize函数在GC时调用来释放这类资源,这个函数默认是空的,不过Object.Finalize是无法重载的,它是根据类的析构函数自动生成的,因此要将释放非托管资源的代码写在析构函数里。
说下GC调用Finalize的流程:
new一个包含Finalize函数的对象,开辟内存后,指向它的指针会被存放到终结列表(Finalization List)。
垃圾回收时,被当作垃圾的对象如果同时存在于终结列表里,就会将该对象从终结列表中移除,并存入终结可达列表(F-reachable List)。并且这些对象变为可达,不会被GC回收,即表示这些对象提升了一代。
该队列中的对象都是可达的,并需要执行Finalize函数。执行Finalize函数是由一个高优先级的CLR线程进行的,执行完毕后,会将对象的指针从Freachable Queue中移除。
当再次进行垃圾回收时,原终结可达列表里的对象经过处理都变为不可达对象,只有当这一代内存不足时才会对对象进行垃圾回收,这些对象内存才会真正释放掉。所以含有Finalize函数的对象最少要经过两次垃圾回收才会被真正释放。
因为非托管资源比较有限,等GC调用Finalize释放,会让非托管资源处于不必要的空闲状态,影响性能。因此定义了一个IDispose接口,用于手动释放非托管资源。
在一个包含非托管资源的类中,关于资源释放的标准做法是:
① 继承IDisposable接口;
② 实现Dispose()方法,在其中释放托管资源和非托管资源,并将对象本身从垃圾回收器中移除(垃圾回收器不在回收此资源);
③ 实现类析构函数,在其中释放非托管资源。
提一下,继承了IDispose接口的类可以使用using,当超出作用域后,系统会自动调用Dispose函数。