小议.Net中的垃圾回收机制

来源:互联网 发布:大富翁炒股软件下载 编辑:程序博客网 时间:2024/05/21 09:47

前段时间在CSDN主页上刊登了一道外国软件公司的笔试题,题目如下:

Given the following,
1.class X2{
2.         public X2 x;
3.         public static void main(String[] args){
4.         X2 x2=new X2();
5.         X2 x3=new X2();
6.         x2.x=x3;
7.         x3.x=x2;
8.         x2=new X2();
9.         x3=x2;
10.         doComplexStuff();
11.}
12.}
after line 9 runs,how many objects are eligible for garbage collection?
A.0  B.1  C.2  D.3  E.4

我将这题转贴到我们学院的论坛上。经过讨论,总结如下:

首先要明确一点,的确是创建三个对象。因为new在IL中就是newobj指令(数组使用newarr指令),而newobj指令就是创建新对象。现在先贴一下新的测试代码,这个测试代码是让程序自主决定什么时候进行垃圾回收(我通过创建大数组让0代满,0代满就会导致垃圾回收,这里要明确一点,创建的数组在.Net中是引用类型,依旧在托管堆中分配内存),而不是先前强行调用GC.Collect来进行回收的。
public class X2
{
        public X2 x;
       
        ~X2()
        {
                Console.WriteLine("End");
        }
       
        public static void Main()
        {
                X2 x2 = new X2();
                X2 x3 = new X2();
                x2.x = x3;
                x3.x = x2;
                x2 = new X2();
                x3 = x2;
                for (int i = 0; i < 20; i++)
                {
                        int[] b = new int[10000];
                        Console.WriteLine(GC.GetGeneration(x3));
                }       
        }
}
先解释这段代码后面的循环,既然我们知道即使在对象名相同的情况下,依然分配新空间。那么这个循环足够导致垃圾回收器第0代满。.Net中垃圾回收一共有3代,分别是0代,1代,2代。每一代有个阀值,第一代的阀值从这个程序来看是介于240KB到280KB间,计算机都喜欢取2的指数,所以我想是256KB为0代阀值。那么当第0代充满时候,就要执行垃圾回收了,这个时候最早声明的两个对象x2和x3将被回收,因为到了后面又分配了一个新对象(名字也叫X2,靠,真是很让人产生错觉的),这个X2的地址覆盖掉旧X2了,导致旧X2在未来的日子已经不会再出现了(这个我们等伙通过IL语言来看就一目了然了),然后X3做为这个新的X2的引用(其实就是新X2的代言人,因为它的地盘就要在0代满后被回收了)。到这里应该很明显可以看出早先X2和X3分配的地盘没人引用。当然就是垃圾回收对象咯。
现在看一下这道程序的Main方法的IL代码:
.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // 代码大小       70 (0x46)
  .maxstack  2
  .locals init ([0] class X2 x2,
           [1] class X2 x3,
           [2] int32 i,
           [3] int32[] b)
  IL_0000:  newobj     instance void X2::.ctor()
  IL_0005:  stloc.0
  IL_0006:  newobj     instance void X2::.ctor()
  IL_000b:  stloc.1
  IL_000c:  ldloc.0
  IL_000d:  ldloc.1
  IL_000e:  stfld      class X2 X2::x
  IL_0013:  ldloc.1
  IL_0014:  ldloc.0
  IL_0015:  stfld      class X2 X2::x
  IL_001a:  newobj     instance void X2::.ctor()
  IL_001f:  stloc.0
  IL_0020:  ldloc.0
  IL_0021:  stloc.1
  IL_0022:  ldc.i4.0
  IL_0023:  stloc.2
  IL_0024:  br.s       IL_0040
  IL_0026:  ldc.i4     0x2710
  IL_002b:  newarr     [mscorlib]System.Int32
  IL_0030:  stloc.3
  IL_0031:  ldloc.1
  IL_0032:  call       int32 [mscorlib]System.GC::GetGeneration(object)
  IL_0037:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_003c:  ldloc.2
  IL_003d:  ldc.i4.1
  IL_003e:  add
  IL_003f:  stloc.2
  IL_0040:  ldloc.2
  IL_0041:  ldc.i4.s   20
  IL_0043:  blt.s      IL_0026
  IL_0045:  ret
} // end of method X2::Main
IL_0000:  newobj     instance void X2::.ctor()
  IL_0005:  stloc.0
上面这两句将旧的X2对象创建后从堆栈弹出存入本地变量0
  IL_001a:  newobj     instance void X2::.ctor()
  IL_001f:  stloc.0
而上面这两句将新的X2对象创建后从堆栈弹出存入本地变量0
结果呢,旧的X2就到此玩完了(等于老大被抓,不过老大管的地盘还在,等伙所管地盘将在0代满后被回收)。
X3已经代言新X2去了,自己的地盘也将在回收之列。
如果你有兴趣的话,可以把内存分配直接分配400k或者4MB,那么新的X2就会被推到第二代,然后一直在第二代里头待着。
      顺便解释一下垃圾回收中的代:当第0代充满时候(超过其阀值),进行垃圾回收,如果不会再被引用的,那么就是回收对象了,如果有被引用的,推到第一代去。然后清空第0代,因为第0代就是用来给新对象分配空间的。接下来,当第0代又充满后,重复刚才的步骤。什么时候回收第一代呢?就是等第一代充满,第一个也有一个阀值,不过这个阀值比较大。当进行第一代回收时候,如果不是被回收的对象,就推入第二代。同理,第二代的回收也是必须等到超过第二代的阀值(这个阀值就更大咯)。总之,第0代的回收是最频繁的。
       如果你分配内存一口气来个400MB,你的硬盘灯就狂闪了,我想这伙应该抛出OutOfMemoryException异常了,这个异常是致命的,CLR如何来具体修复我暂不清楚,总之你可以看到你的计算机慢下来,硬盘灯狂闪N秒。
        那么那个新X2什么时候被回收呢,那就是等AppDomain卸载的时候,所以你在Main执行到它的花括号结束后,就会发现又冒出个end了。

然后下面是另一个同学对我的总结做的一个补充:

研究表明大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成三个段,称作generations。新分配的对象被放在generation 0中。这个generation是最先被回收的——在这个generation中最有可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器的L2 cache中),因此在它里面的回收将是最快和最高效的。
        当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操作被触发。由于generation 0的大小很小,因此这将是一个非常快的GC过程。这个GC过程的结果是将generation 0彻底的刷新了一遍。不再使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。
       当generation 1的大小随着从generation 0中移入的对象数量的增加而接近它的上限的时候,一个回收动作被触发来在generation 0和generation 1中执行GC过程。如同在generation 0中一样,不再使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,因为在generation 0中最有可能存在大量的已不再使用的临时对象。对generation 2的回收过程具有很高的开销,并且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。如果对generation 2的GC过程仍然不能释放足够的内存,那么系统就会抛出OutOfMemoryException异常。

 

原创粉丝点击