喂鸡百科 翻译:
追踪垃圾回收是一种自动内存管理,这种机制决定了什么对象应该被回收,除了从根作用域开始的引用链上可到达的对象外,其余对象一律被认为是“垃圾”而且应该要回收。垃圾回收经常都是采用这样的方式实现,而不是采用引用计数方式。
一个对象的可达性
一个可到达的对象准确来说有以下两种情况:
1.所有可以在调用栈中引用的对象(包括当前执行函数中的参数、函数中的本地变量),以及所有全局变量【ps:如f函数没执行完,开始执行垃圾回收,则f里的东西就是可达的】
2.所有具备可达性的对象所引用的对象,也具备可达性。
“垃圾”不具备可达性,我们不再使用这个数据而且它离开了它的作用域。和“语法垃圾”不同,“语法垃圾”是程序中不可能访问到的对象,而“语义垃圾”是程序中不可能用的对象,如:
Object x = new Foo(); Object y = new Bar(); x = new Quux(); if(x.check_something()) { x.do_something(y); } System.exit(0);
对于前三行代码来说,Foo实例对象就是后续的代码中无法访问到的“语法垃圾”,对于if语句块,y可能是“语义垃圾”,因为if条件可能不执行,这样y就用不到了。
对于具备引用类型和基础类型的语言来说,垃圾回收器需要采取某一种方式去区分栈中的变量或者对象的字段他们属于什么类型以及他们引用了什么;在内存中,一个整数和一个引用可能很类似。垃圾回收器需要去弄清楚元素什么情况下是引用类型,什么情况下是基本类型,一种通用的解决方案就是使用“标记指针”
强类型和弱类型引用
弱引用:用于引用存在的对象,而且不会延长这个对象的生命周期
强引用:当对象没有强引用的时候,即使这个对象上有弱引用,垃圾回收器也会回收这个对象
基础算法
垃圾回收会周期地执行,当内存不足以满足一次分配请求时,垃圾回收会被马上触发。垃圾回收执行的操作是“标记和清除”
标记清除法
内存中每个对象都有一个标记(通常只会占用1比特大小),这个标记只会被垃圾回收器所读取使用,在垃圾回收期间外,所有对象都处于未标记状态(cleared)
第一阶段就是 标记阶段 ,以遍历树的形式遍历作用域树,每个作用域中被直接指向的对象会被进行标记,所有其他对象如果当前作用域中被已标记对象所引用,则也会被进行标记。这样所有可以直接通过根作用域访问到的对象都会被标记。
第二阶段就是 清除阶段 ,整块内存会被重新扫描。没有被标记的对象意味着他们不能被任何某个作用域所访问到,那就释放那些对象的空间。接着把之前标记了的对象的标记进行清除,准备下一次的垃圾回收。
这个方式有多个缺点,其中最主要的是垃圾回收过程中,不允许内存工作集发生任何变化,导致系统会处于停滞状态(触发时机通常是不可预料的)。这使“实时”和“时序严格”变得不可能。总的来说,整块内存必须被检查,而且还要检查两次,对于分页系统来说可能会造成其他问题
剩余的以后再补充
案例分析
语法垃圾
function foo() { this.var1 = "potential accidental global"; } // Foo作为函数调用,this指向全局变量(window) // 而不是undefined foo(); function foo(arg) { bar = "some text"; }
以上的foo函数中,都无意地创建了一个全局变量,这是我们用不到的,出现内存泄漏
循环引用
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 引用 o2 o2.p = o1; // o2 引用 o1 形成循环引用 } f();
以上例子如果使用引用计数,则f每调用一次,则两个对象会出现内存溢出。如果使用标记清除法,函数执行时他们两个对象会被标记,所以不会被回收;当函数结束后,因为两个对象都没有被外部可达对象所引用,所以他们不会被标记,会被回收。