在博问里看到有人在问关于GC的问题,便自己也发了个回复赚点园豆。今天阳光明媚啊,又是个艳阳天呢。转入正题:
什么是"GC"?
垃圾收集器GC(garbage collector)。对于这个概念应该是所有.net学习这都知道的,它使得程序对于内存的分配、回收都不需要程序员操作,程序员只要需要的时候new就可以,用完了不用管,有人帮你清除不用的内存。这个人就是“垃圾收集器”。.Net程序员一般不用像C++程序员那样处理内存的申请和回收,是因为有GC。知道概念我们就该细致的总结一下它的详细特性。
对象的生存周期
1.当我们创建一个对象的时候,运行构造函数,为其分配一块内存,我们称之为一个活对象。
2.当一个对象或者它的任何部分在后续的程序执行过程中不再可能被访问的时候,它就被视为不再被使用的对象,满足了销毁的条件,GC就会做出判断,该对象是否满足回收的条件,之后开始干活,你懂得。
3.当GC觉得你这个对象应该挂了,他就会启动这个对象的自杀式武器(如果该对象有析构函数)来自我结果,至于他什么时候要你挂,完全要看他的心情,或许就是她觉得你这个对象没什么利用价值了,正所谓飞鸟尽良弓藏。
4.当你这个对象运行析构函数(要挂)的时候,可能在存在运行析构函数的时候在析构函数内部重新指向了一个对象的情况。所以只有当该对象包括在析构函数的一切后续程序中都在没有可能被访问的时候GC才会认为是满足了回收条件。
5.当对象满足被回收的条件的时候,GC就会释放对象所占的所有内存。
下面我们做一个小小的测试来验证当对象没用了,GC时候要你挂,完全要看他的心情
运行效果:
上面的代码我们只是编写了创建对象的代码没有编写销毁对象的代码,但是对象被自动的销毁了,而且销毁的次序不是我们自己掌握的。
能否自己控制垃圾回收?
说到这里我们是否考虑到能否自己控制垃圾回收呢,答案是肯定的,我们可以使用GC类提供的GC.Collect方法来使应用程序在一定程度上直接控制垃圾回收器,但是一般不要去手动干预GC。没有特殊理由,不要去调用GC.Collect(),让它自己决定什么时候去回收内存。还是人家的比较严谨。
是的,我们可以在确定某个时间点的内存使用量大量减少的时候叫服务员过来收一下盘子(GC.Collect()一下),但是如果我们频繁的叫服务员过来清理垃圾是不是会影响我们吃饭呢,这就好比,服务员过来收拾垃圾,对大家说,先生们先别吃请抬一下手,我先擦一下桌子,接着刚又下手吃的还没尽兴,又有个同桌小妞叫服务员有过来收拾垃圾,又说先生们再抬一下手,姐又要收拾下空盘子...!#$@,你妹呀,自己喊过来喊过去的,原来还是很影响自己吃饭呀,以后一定要有个约定积累到一定的量在叫服务员过来服务,这样还不如用他们店里自己的回收垃圾规则啊,人家毕竟是干这行的,顾客至善,人家啥时候来GC那才叫专业。所以还是在遇到服务员开小差le再去叫她,一般不要主动叫啦。
这个叫小姐过来服务的的过程她会要我们先停手,这就可以很好理解,在垃圾回收器执行回收之前,它会挂起当前正在执行(吃)的所有线程。如果不必要的多次进行GC.Collect()方法,则可能(影响吃饭)降低程序执行的性能。从而人为地削弱了垃圾回收器本身优化引擎的作用。
下面我们来自己叫一下服务员(GC.Collect()一下)
我们来看这段代码:
using System; namespace GCTest { class Test { static void Main(string[] args) { Person p1; for (int i = 0; i < 10000; i++) { p1 = new Person(); } Person p2; for (int i = 0; i < 10000; i++) { p2 = new Person(); } } } class Person { public Person() { } } }
现在我们生成后使用工具来看看它的生成实例和资源的占用情况
现在我们还是认为的对我们所认为没有对要继续存在的对象进行清理,我们再来看看结果,现在我们发起造人和毁人运动
1 using System; 2 3 namespace GCTest 4 { 5 class Test 6 { 7 static void Main(string[] args) 8 { 9 CreatePerson();//在内存中创建一些对象 10 long memory1 = GC.GetTotalMemory(false); 11 Console.WriteLine("回收之前已用内存{0}",memory1); 12 GC.Collect();//回收 13 long memory2 = GC.GetTotalMemory(true); 14 Console.WriteLine("回收之前已用内存{0}", memory2); 15 Console.WriteLine("释放的内存大小为{0}",memory1-memory2); 16 Console.ReadKey(); 17 } 18 19 private static void CreatePerson() 20 { 21 Person p1; 22 for (int i = 0; i < 10000; i++) 23 { 24 p1 = new Person(); 25 } 26 Person p2; 27 for (int i = 0; i < 10000; i++) 28 { 29 p2 = new Person(); 30 } 31 } 32 } 33 class Person 34 { 35 public Person() 36 { 37 } 38 } 39 }
运行结果如下:
我们再看看调优软件分析的结果,会发现时间内存占用在某一时刻全部释放了。
非确定性销毁
在前面的内容里,我们说到了GC对于程序何时销毁的时间是我们自己不可预测的,完全由GC自己决定,同时又说道即使某个对象符合了被销毁的条件但是任然可能不符合回收的条件。
GC决定何时回收对象和运行析构函数方面可以有很大的选择范围,因此相同的代码在不同的情形(内部环境)中运行的结果可能不一样。
C#并不一定要求运行析构函数,不要求对象一符合回收条件就立即回收,垃圾回收器自己决定何时运行对象的析构函数,销毁对象的时间不是有程序本身决定的,这就是所谓的非确定性销毁。
下面我们对象这两中情况一一做下小试验
using System; namespace GCTest { class Test { static void Main(string[] args) { Parent 刚哥 = new Parent(new Children()); 刚哥 = null; GC.Collect(); GC.WaitForPendingFinalizers(); //挂起当前线程,直到处理终结器队列的线程清空该队列为止。 Console.ReadKey(); } } //注意一下两个类相互独立没关系,之后为了下一个试验随便命名的 class Children { ~ Children() { Console.WriteLine("销毁Children类的对象实例"); } } class Parent { object protecteObject;//保护对象 public Parent(object o) { protecteObject = o; } ~Parent() { Console.WriteLine("销毁Parent类的对象实例"); } } }
输出结果可能是:
销毁Parent类的对象实例
销毁Children类的对象实例
或者
销毁Children类的对象实例
销毁Parent类的对象实例
“符合销毁条件并不意味着符合回收条件”
using System; namespace GCTest { //注意一下两个类相互独立没关系,之后为了下一个试验随便命名的 class Children { ~ Children() { Console.WriteLine("销毁子类的对象实例"); } public void CallFather() { Console.WriteLine("老爸:要我出来我要重新做会自己,在活跃几天!"); Test.小李 = this;//让其重新指向Test.小李 } } class Parent { public Children protecteObject;//保护对象 ~Parent() { Console.WriteLine("销毁父类的对象实例"); protecteObject.CallFather(); } } class Test { public static Children 小李; public static Parent 刚哥; static void Main(string[] args) { 刚哥 = new Parent();//建一个老爸出来 小李 = new Children();//再蹦出个儿子 刚哥.protecteObject = 小李;//刚哥的保护对象是小李 刚哥 = null; 小李 = null; //现在小李和刚哥都满足销毁条件了。 GC.Collect(); GC.WaitForPendingFinalizers(); //刚哥符合回收条件但是小李不符合 if (小李 != null) { Console.WriteLine("小李依旧活跃!"); } Console.ReadKey(); } } }
注意:以上举得例子中没有继承关系,命名只是完全是好玩,都是独立的类。
运行结果如下:
这是为什么呢??
原来在运行Parent类的析构函数时候,使先前没有考生的小李,洗白了下省份,变得可从当前有效地引用Test.小李,换了个身份,本质还是坏坏的,嘿嘿,还是Chind类的一个实例。
写程序时为了避免这种意外,析构函数应该只清理本身的实例字段而不要与外加挂钩去影响别的实例,不然就让我们对有些情况变得混淆和感到意外了,还可以使用弱引用【WeakReference】来预知一个对象实例是否又没有被回收。
结束语:也就这样完了啦,最好还是利用好GC本身优化引擎的作用.