前段时间在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异常。