垃圾回收机制
垃圾回收基于两个原理:
考虑某个变量或对象在未来的程序运行中将不会被访问
向这些对象要求归还内存
而这两个原理中,最主要的也是最艰难的部分就是找到“所分配的内存确实已经不再需要了”。
垃圾回收方法
- 引用计数(reference counting) 在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收。上例子:
let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1
let obj2 = obj1; // A 的引用个数变为 2
obj1 = 0; // A 的引用个数变为 1
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了
但是引用计数有个最大的问题:循环引用。
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。
要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null;
obj2 = null;
- 标记-清除(mark and sweep)
这是 JavaScript 中最常见的垃圾回收方式。为什么说这是种最常见的方法,因为从 2012 年起,所有现代浏览器都使用了标记-清除的垃圾回收方法,除了低版本 IE…它们采用的是引用计数方法。
那什么叫标记清除呢?JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象…对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。
标记-清除法的一个问题就是不那么有效率,因为在标记-清除阶段,整个程序将会等待,所以如果程序出现卡顿的情况,那有可能是收集垃圾的过程。
2012 年起,所有现代浏览器都使用了这个方法,所有的改进也都是基于这个方法,比如标记-整理方法。
标记清除有一个问题,就是在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。计算机中的很多做法都是互相妥协的结果,哪有什么十全十美的事儿呢。