最近一直在看clr via c#这本书 在核心机制之垃圾回收这章中 遇到了一个特别纠结的问题:
书的463页一段话:“类型 中定义的任何静态字段被认为是一个根,除此之外,任何方法参数或局部变量也被认为是一个根 只有引用类型的变量才被 认为是根 值类型的变量永远不被认为是根”
这短话让我非常纠结 方法参数和局部变量也有可能是值类型啊 这不 互相矛盾了麽???
------------------------------------------
还一个疑问 在书的466页:”类型的静态字段永远是它引用的任何对象的根“
这句话怎么理解呢 静态字段如何来引用一个对象呢 其实我对于根的理解也比较模糊 恳求大牛们解答!
"只有引用类型的变量才被 认为是根 值类型的变量永远不被认为是根" 是对之前的补充,当“方法参数和局部变量是值类型”的时候,它们不会被认为是根。以下的可能会帮助你理解什么是根:
(1)GC可能在2个时候发生,一个是newobj的时候,一个是显式GC的时候,无论如何,肯定是由某个方法的某句代码引起的(一句new或者一句GC.xxxx)。
(2)当GC发生的时候会对所有堆上的对象进行标记工作,根据标记的结果来判定哪些是有用的,哪些是没用的。由于对象之间的引用关系很复杂(甚至存在互相循环引用),因此标记工作一定得有起点,这些起点就是根。例如根是[a,b,c]3个对象(先姑且不管为什么是它们3个),先考察a对象,a引用了e,因此e也被标记为有用的,e引用了f,f也被标记了……最终这个“深度优先搜索”结束了。回到b做同样的事情,然后是c。最后堆上的对象被分成了2组,1组是“从根作为起点,可以到达的(本质是存在引用路径关系的)”,另外一组是无法到达的(因此就是垃圾,可以被收集了)。这就是根的意义。
(3)现在来讲为什么静态字段要被作为根。其实这个我个人觉得是很容易理解的,因为静态字段在整个应用程序中是随时都可以被访问的。假设有这么个类:
public class Foo { public static Bar InstanceOfBar = new Bar(); pubilc void A() { //do something } public void B() { //do something } }
Foo.InstanceOfbar引用了堆上的一个Bar对象,那么这个对象无论如何都是不能被回收(直到当前应用程序结束),因为Foo.InstanceOfBar可以在任何时候被访问到。而这里假设去掉static,改成实例字段,那么这个bar对象会在它所在的foo对象被当作垃圾处理之后被标记为垃圾(因为除了那个foo对象能带领GC标记到达它之外,它无法被任何对象“到达”)。
(4)参数和局部变量也很好理解。先说参数:参数是从调用者(外部)传过来的,从它顺藤摸瓜,很容易把它的上一层,上上层(一直到应用程序入口函数)的所有有关系的(有用的)对象都标记出来。局部变量是当前方法体新定义的,它们指向的堆上的对象在此时必然是有用的(并且对象引用的对象也是有用的)。这3者能确定所有有用的对象,甚至会有重复,例如:
private void M(Foo f) { Bar b = f.Bar; }
这里无论是以f为根(起点),或者以b为根,都可以得出“f.Bar这个对象是有用的"这个结论。因此这3者可以完美覆盖所有的有用的对象,剩余的必然是可以被GC的。
首先非常感谢您的精彩回答!
对于“静态字段永远被认为是根 我想我已经明白了 因为静态字段在整个应用程序的生命周期内都一直存在 那么它所引用的对象一定不能被视为垃圾 ” 但是 为什么一定要是方法里面的局部引用变量才被认为是根呢? 方法外面的变量就不行麽 这个我不明白 一个方法调用结束以后 在方法内部定义的引用变量所指向的对象 会一直常驻托管堆而不释放麽?
@果果天涯: “方法外的变量”是指什么?你给我举个例子,给出代码。
@水牛刀刀:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Collections; 6 using System.Text.RegularExpressions; 7 8 namespace ConsoleApplication2 9 { 10 class Program 11 { 12 List<string> list = new List<string>(); 13 static void Main(string[] args) 14 { 15 Console.ReadLine(); 16 } 17 public void Test() 18 { 19 20 } 21 22 } 23 }
比如说上面的 list 变量 不是Test方法外面的变量麽?
我知道我可能对这些概念有严重的误差 恳请刀刀不吝赐教 非常感谢!
@果果天涯: 说list是变量不准确,应当说“list是Program类的成员(字段)”。你调用Test方法时:
var p = new Program(); p.Test();
假设在Test()中引起了一次GC,此时this(就是执行Test方法的当前实例,也就是p)是根,因此this.list(就是p.list)被标记成有用的,不会被GC。所以,你所认为的“方法外的变量不会被GC”,本质上是由于这个“字段”所属的那个实例不会被GC。
@水牛刀刀: 我我 我再仔仔细细考虑 虽然我心中还是很多疑问 也许我对于“方法”这个概念一开始都没弄清楚 我觉得依据您的说法 实例字段 是由于这个实例存在 因此此实例字段不会被回收 这个是自然 可是 一个方法调用结束以后 方法内部的局部变量所引用的对象 为什么不能被回收呢? 还是可以这么认为:垃圾回收器在遍历应用程序的根的时候 是“遍历那些正在使用中的方法的局部变量” 而那些方法调用已经结束的局部变量 不当作它是根? 可以这么理解麽?ps:非常感谢您前面的耐心解答
@果果天涯: 方法调用结束之后局部变量在GC时肯定是被标记成垃圾的,书中说的”方法“显然是指当前正在执行的方法啊!
class Program { static void Main(string args[]) //入口方法 { var something = new Something(); A(); B(); } static void A() { var a = new ItemA(); //do something } static void B() { var b = new ItemB(); C(); //do something } static void C() { //GC here } }
假设在C中发生了一次GC,那么此时的根并不包括A方法中的变量。因为此时活跃的方法是Main -> B -> C,此时的根是b,something,还有main的参数args。如果你真的认真看过CLR via C#,那么应该知道一个方法执行完成后栈帧会展开(就跟执行这个方法前一样),A方法执行完成后,栈上已经没有任何A方法的痕迹了。
@水牛刀刀: 非常感谢您的耐心解答!
根说法好象是有点比喻的感觉