C# - 垃圾回收机制(*)值与引用,托管与非托管资源 088

在.NET Framework 中,内存中的资源(即所有二进制信息的集合)分为“ 托管资源 ”和“ 非托管资源

C#中数据分为值类型与引用类型;

值类型:数据存储在栈中;属于非托管资源,不需要通过垃圾回收机制管理

引用类型存储在栈与堆中(对象的引用存储在栈中,数据存储在堆中)其中堆中的数据属于托管资源,需要由垃圾回收机制进行管理(相当于由"系统自动管理内存")

在.Net的CLR(通用语言运行时)中有一个核心功能即垃圾回收机制(简称GC),其作用就是回收托管堆中的内存资源,一般由系统自动调用(也可以手工调用,但不建议这么做)可以回收C#中的引用类型在托管堆中的资源(所占内存),值类型不用管(这个不需要GC,会自动释放)

“非托管资源”:需要由程序员手工调用;如文件句柄,数据库连接,网络端口等资源

"垃圾回收”机制是 .NET Framework 的特性,而不是C#的 ,需要注意的是:

1)值类型(包括引用和对象实例)和引用类型的引用不需要通过GC来释放内存,因为当它们出 了作用域后会自动释放所占内存

2)GC只对引用类型的引用所指向的对象实例保存的托管堆资源起作用

3)GC并不是实时进行回收(不是没有没有对象引用立马进行回收)

垃圾回收的目的 :1)使程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题 2)大大减少了内存人为管理不当所带来的Bug 3)最终目的提高内存利用率

C#中垃圾指的是什么:引用类型中没有变量(栈中的变量)引用的对象(堆中的对象)表示可以GC进行回收的连接(不是立即进行回收)

垃圾回收的机制:GC中 使用了mark-and-compact(标记和压缩)算法,在GC中有"代"的概念(下面细说)对可以暂时存活的对象进行标记,没有进行标记的会被GC回收:压缩:是将被GC回收后留出的不连续的空间,移动到一起(形成连续的空间)类似电脑的碎片整理

//定义Person类,类中有一个Age属性
示例1:没有变量引用的对象,表示可以被回收的"垃圾"
Person p1 = new Person();
p1.Age = 18;
Person p2 = new Person();
p1 = p2;当执行了这句话
p1指向的对象(堆中的对象),已经没有变量指向了
因此p1就变成了可以被回收的"垃圾"
相当于 Person p1 = null;
//============================
示例2:即使设置了null,依然不能被回收
Person p3 = new Person() { Age = 18 };//对象初始化器
Person p4 = new Person() { Age = 20 };
Person[] ps = new Person[] { p1, p2 };
p1 = null;//即使没有引用的对象
//但是还有一个ps[0]引用着p1对象,所以还是不能被回收
Console.WriteLine(ps[0].Age);

//进行手动进行回收
GC.Collect();//对所有"代"进行强制回收
//最好不要进行手动调用使其进行强制回收,
//因为垃圾回收机制自己有一系列的算法策略,需要移动对象等等
//进行强制回收,为了达到目的,需要暂停应用程序的已处理
//如果频繁的调用垃圾回收会影响整个程序的性能
Console.ReadKey();

C#-垃圾回收机制,*值与引用,托管与非托管资源088

示例,释解图

.NET将heap(用于分配管理)分成3个代龄区域:Gen 0,Gen 1,Gen 2,按照对象的生命周期,将对象分为新旧两种情况,对不同的情况使用不同的回收算法与策略,加强对新区域的回收处理力度,争取在较短的时间间隔,较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉,释放占有的内存

分代的益处: 避免每次回收遍历所以的对象,减少逻辑回收时间,提高工作效率

GC有3种方式 :#Gen 0 collections,#Gen 1 collections,#Gen 2 collections;

如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1;

如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2

  Gen2的GC将Gen 0 heap,Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高;粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,full GC可能需要花费几秒时间,因此大致上来讲.NET应用运行期间,2代,1代和0代调用GC的频率大致为1:10:100

C#-垃圾回收机制,*值与引用,托管与非托管资源088

GC内部示意图

0代假设设置了200KB内存,只能存储5个对象,当0代中储存的对象达到阈值(设置的临界值)GC会自动将没有引用的对象进行回收(如p2,p4),0代中的幸存者(p1,p3,p5)会被移动到1代中,剩下的会进行"压缩处理"即p1nullp3null,p5一>p1,p3,p5,null,null,再有新的对象存放在后两个null中,下面两代中都会经历类似的"压缩处理",将断续的空间整理为连续的空间

当1代中储存的对象也达到阈值(假设是2MB内存),GC会自动将没有引用的对象进行回收(如p2,p4),0代中的幸存者(p1,p3,p5)会被移动到1代中;其中p11-p13是0代又达到阈值被GC清理过的幸存者,当1代中的对象即使经过GC清理还是达到了阈值,就会将1代中的幸存者移动到第2代

当2代中储存的对象达到了阈值(假设是2GB内存)假设又有要储存新的对象(假设1MB)而GC无论怎么回收0,1,2三代中的"垃圾"内存中都不没有1MB的空间就会报异常(类似Out of Memory内存不够用)

注意:0代不一定非到阈值才会回收(不定时回收)1,2代是达到阈值才会回收;优先回收第0代,没有回收的移动到第1代,1代没有回收的移动到2代,都满了2代会试着扩展,如果内存不够扩展所需就报异常

程序中除了被托管的内存资源由GC处理,其他非托管资源可以通过~析构(或终结)方法或实现接口 IDisposable Dispose ()方法释放内存(再比如使用using,之后会举例说明)

 class Class1 : IDisposable
 {
     #region IDisposable 成员
     public void Dispose()
     {
         //这里的代码,来释放除内存资源外的其他资源
     }
     #endregion
     //通过反编译工具查看
     //在c#中叫Finalize()函数 中文:终结函数(析构方法)
     //在当前对象被垃圾回收之前会调用Finalize()函数,释放其他资源(如数据库连接等).
     ~Class1()
     {
         //无法手动调用,一般用于回收其他内存,都写在Dispose()方法中
         //既然Dispose()方法已经释放其他内存,这时,就不再调用Finalize()方法了,
         //所以就通过 GC.SuppressFinalize(this);告诉程序不调用Finalize()了
         
          //通过查看Stream类的Dispose()
         //GC.SuppressFinalize(this)方法位于Close()方法中
         //Close()方法位于Dispose()方法中
     }
 }