C#垃圾回收和托管堆
每次使用一种资源,都必须要为这种资源类型分配内存
资源的生命周期
调用IL指令newobj,为生成的类型分配内存(一般使用c#new 操作符完成)
初始化内存,设置内存状态并使资源可用,类型的构造器负责设置初始状态
访问类型的成员来使用资源
摧毁资源状态以进行清理
释放内存。垃圾回收独自负责这一步
C#的new操作符会导致CLR执行以下步骤
计算类型字段所需的字节数,加上对象开销所需的字节数
检查区域中是否有可以分配这个对象所需的字节数,如果空间足够,就放入对象,当空间不足时,CLR就会执行垃圾回收
垃圾回收算法(CLR使用的是引用跟踪算法)
引用计数
当一对象被另一个对象引用时或不再被引用时,引用计数就会相应的增加或减少
使用引用计数算法只需要遍历所有引用类型变量,当引用计数为0时就表示没有任何对象引用该对象,可以进行回收
引用计数最大的问题就是无法处理循环引用,比如2个或多个对象互相引用,但这些对象都不再被外部使用,这时这2个对象的引用计数都不会变为0,因此也无法被回收
引用跟踪
引用跟踪算法只关心引用类型的变量,所有引用类型的变量都被称为根
当进行垃圾回收时,CLR会检查所有根,如果一个根包含null则忽略,并检查下个根
如果不为null,则这个对象就会被标记,并且递归检查这个对象的所有引用,如果这些引用不为空则也会被标记
在标记过程中如果在标记某个对象时发现它已经被标记,那么会使用循环引用检测算法检测这个对象是不是循环引用
检查一遍后,被标记的对象就是可达对象,没有被标记的对象和被定义为循环引用的对象就是不可达对象,不可达对象就可以进行回收
代
托管堆会被分成3个部分,分别称为0代,1代,2代
当新对象生成时,对象会被分配到0代内存中
如果一个新的对象分配后,造成0代超预算,那么就会触发垃圾回收
在垃圾回收后,0代内存中存活的对象会被转移到1代内存中
在不断的内存迁移中,1代内存也会慢慢扩大,所以当1代内存容量达到预算时,垃圾收集器就会回收0代和1代中的对象
在垃圾回收后,1代内存中存活的对象会被转移到2代内存中
当2代内存也达到预算时,CLR就会触发所有区域的垃圾回收
当3个区域都被占满时,应用就停止响应了
将托管堆分区的好处
垃圾回收时,回收一部分,速度快于回收整个堆
存活时间长的对象大概率是不需要回收的,在每次回收时将幸存的对象向后迁移到后面的内存区域,这样就可以保证,前几个内存区域储存的对象都是新对象,进行垃圾回收时找到垃圾的几率也更高
3个内存区域并不是平均分配的,而是动态调节的
如果在回收0代内存后存活下来的对象很少,这时就会减小0代内存的预算,空间小意味着垃圾回收会更频繁,但每次回收做的事也少了
如果回收0代内存后发现还有很多对象存活,这时就会扩大0带内存的预算,这样垃圾回收的次数将减少,但每次回收时,回收的内存也越多
在其它2个内存区域也会使用类似的算法控制预算
垃圾回收触发条件
创建对象时,发现空间不够
当任何一个内存域的内存超出预算时
代码显式调用GC.Collect方法,这样会触发所有托管堆的垃圾回收,,并且垃圾回收是自动触发的,一般不需要显示调用
系统报告可用内存低时