在.NET程序开发中,为了将开发人员从繁琐的内存管理中解脱出来,将更多的精力花费在业务逻辑上,CLR提供了自动执行垃圾回收的机制来进行内存管理,开发人员甚至感觉不到这一过程的存在。.NET程序可以找出某个时间点上哪些已分配的内存空间没有被程序使用,并自动释放它们。自动找出并释放不再使用的内存空间机制,就称为垃圾回收机制。
一、垃圾回收机制
每当有对象新建时,公共语言运行时都会从托管堆为对象分配内存。 只要托管堆中有地址空间,运行时就会继续为新对象分配空间。
不过,内存并不是无限的。.NET程序可以找出某个时间点上哪些已分配的内存空间没有被程序使用,并自动释放它们,自动找出并释放不再使用的内存空间机制,就称为垃圾回收机制(Garbage Collection,简称GC)。
应用程序对内存回收的方式,常见的一般有如下几种:
- 手动管理:C,C++
- 计数管理:COM
- 自动管理:.NET,Java…
但是手动管理和技术管理的复杂性很容易产生以下问题:
- 忘记释放内存
- 访问已经释放的内存
无法自动化的内存管理方式极容易产生bug,影响系统稳定性,尤其是线上多服务器的集群环境,程序出现执行时bug必须定位到某台服务器然后dump内存再分析bug所在,极其打击开发人员编程积极性,而且源源不断的类似bug让人厌恶。
.NET为了将开发人员从繁琐的内存管理中解脱出来,启动自动执行垃圾回收的机制来进行内存管理。
二、垃圾回收条件
1、堆进行内存分配时,堆上的可用内存不足时触发GC;
2、代码主动显式调用System.GC.Collect();
3、其他特殊情况,比如,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收。
三、.NET中的GC
垃圾回收机制的主要工作是找出堆空间分配的空间中哪些空间不再被程序使用,然后回收这些空间。在.NET中使用的方式,是最主流的"标记并清除"的方式,同时为了提高GC回收效率与性能,.NET使用了以下GC回收优化策略:
1、分代
.NET中将引用类型的对象分为三类,分别是第0代、第1代与第2代。
第0代中的对象存活时间通常最短,第1代中的对象存活时间比较长,第2代中的对象存活时间最长。
分代依据的目的是,尽量增加每次执行垃圾回收处理时,可回收的对象的数量,并减少处理所需的时间。
2、压缩
反复执行分配与回收操作,可能导致堆上产生很多空余空间,这些空余空间又被称为碎片空间。
压缩机制可以通过移动已分配空间把碎片空间合并到一块,使得堆可以分配更大的对象。
.NET运行时提供的GC是支持压缩机制的,但是只在一定的条件下启用。
3、大小对象
因为移动大对象需要的成本很高,.NET根据引用类型对象值占用的空间大小来区分是小对象还是大对象。
大对象与小对象会在不同的堆区域中分配:大对象堆和小对象堆。
前面我们提到的压缩机制,默认只在小对象堆启用,大对象堆是不会执行压缩的。
4、 固定对象
托管代码传递引用类型对象给非托管代码时必须创建固定类型的GC句柄,并在托管代码中保持这个句柄存活到非托管代码的调用结束。
创建了固定类型GC句柄的对象就称为固定对象。
使用固定对象会带来一些副作用,那就是由固定对象带来的碎片空间是无法合并的。
5、析构队列
如果在垃圾回收的过程中执行这些析构函数,垃圾回收需要的时间是不可预料的。
如果对象不再存活但定义了析构函数,那么对象会添加到析构队列并标记存活。
析构函数执行完毕的对象,可以在下一轮GC中被回收。
6、STW(Stop The World)
对象之间的引用关系会随着程序运行不断改变,让执行GC的线程与执行其他处理的线程同时运行会带来一些问题。
让执行GC处理以外的线程全都暂停运行,像这样的停止操作我们称为STW(Stop The World)。
7、普通GC与后台GC
普通GC会导致更长的单次STW停顿时间,但消耗的资源比较小,并且支持压缩处理。
后台GC每次STW停顿时间会更短,但停顿次数与消耗的资源会更多,并且不支持压缩处理。
四、工作站模式与服务器模式
工作站模式,适用于内存占用量小的程序和桌面程序,它可以提供更短的响应时间。
服务器模式适用于内存占用量大的程序与服务程序,可以提供更高的吞吐量。