你可能听说过JAVA、.NET、PHP这些语言有垃圾回收的内存管理机制,但是很少会听到JavaScript也有自己的内存管理机制,JavaScript同样有着类似的垃圾回收功能。本文主要讲述了JavaScript的垃圾回收原理和具体的过程。
简介
在底层语言中,比如C,有专门的内存管理机制,比如malloc() 和 free()。而Javascript是有垃圾回收(garbage collection)机制的,也就是说JS解释器会自动分配和回收内存。这样就有人觉得,我用的是高级语言,就不用关心内存管理了,其实这是不对的。
内存的生命周期
尽管语言不尽相同,而每种语言中内存的生命周期都是相似的:
1.当需要的时候分配内存
2.对内存进行读写操作
3.当上面分配的内存不再需要的时候,将他们释放掉
对于1,2两步,几乎所有语言操作起来都是明确地或者说很直观,没什么好说的。而在像Javascript一样的高级语言中,第三步操作就显得不那么直观。
Javascript中分配内存空间
变量初始化
当变量初始化的时候,Javascript会自动分配相应的内存空间(注:这里MDN上关于这里用的是Value initialization,到底是声明,还是在赋值时候分配空间,还要再学习一下)
var n = 123; // 为数字分配空间
var s = “azerty”; // 字符串
var o = {
a: 1,
b: null
}; // 为对象和它包含的属性分配内存空间
var a = [1, null, "abra"]; // (类似对象)给数组和它里面的元素分配空间
function f(a){
return a + 2;
} // 为函数分配空间
// 函数有时也会为分配对象空间
someElement.addEventListener(‘click', function(){
someElement.style.backgroundColor = ‘blue'; //个人补充,未考证,这里会为someElement分配空间,如注释所说,为对象分配空间
}, false);
函数调用时候分配空间
有的函数调用,会产生上面说的那种 为对象分配空间
var d = new Date();
var e = document.createElement('div'); // allocates an DOM element还有下面这种
var s = “azerty”;
var s2 = s.substr(0, 3); // s2 is a new string
// 由于Javascript中字符串是不可变的,所以Javascript也许并没有为s2中的字符串分配新空间,而是只存了[0, 3]的区间(用来索引)
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 新的空间来存储数组a3
操作变量值
没什么好说的,读、写、函数调用。
内存不再被使用时,将它们释放掉
许多内存管理机制的问题都出现在这里。最麻烦的问题是确认“这块内存空间已经不需要了”。这往往需要程序员告知,这个程序中,这块内存已经不需要了,你们回收吧。
而高级语言解释器中嵌入了一个叫做“垃圾回收(garbage collector)”的工具,用来跟踪内存分配和使用情况,以便在它们不需要的时候将其自动回收。然而有个问题,一块内存空间是不是还有用,是具有不确定性的,也就是说,这个是没法用算法精确算出来的。
垃圾回收
如上所述原因,垃圾回收机制采取了一种有限的解决方案来处理上面的不确定性问题。下面介绍集中垃圾回收算法的思想以及相应的局限:
引用
这种方法,用到了一种引用的思想。当a能访问A时,就说A引用了a(不论是直接还是间接的)。比如,一个Javascript对象会引用他的原型(间接引用)和它的各个属性(直接引用)。
这种情形下,对象就被扩展的更广义了,在原生对象的基础上,还包含了函数的作用域链(或者全局的词法作用域)。
引用计数
这种方法是最拿衣服(naive)的垃圾回收算法。它把“可以回收”的标准定义为“没有其他人引用这个对象”(原文:This algorithm reduces the definition of “an object is not needed anymore” to “an object has no other object referencing to it”)。也就是说,只有当对象没有被引用的时候,才会被当作垃圾回收掉。
举个例子
var o = { // 称之为外层对象
a: { //称之为内层对象
b:2
}
}; // 创建了两个对象 内层对象作为外层对象的属性而被引用
// 而外层对象被变量o引用
// 显然,没有人会被垃圾回收
var o2 = o; // o2也引用了上面說的外層物件。好現在外層對象的引用計數為'2' (被o和o2引用)
o = 1; // 現在o不再引用外層對象,只有o2在引用,引用計數為'1'
var oa = o2.a; // oa 引用內層物件
// 現在內層物件同時被引用為外層物件的屬性和oa引用,引用計數為‘2'
o2 = “yo”; // 好,現在o2也不引用外層物件了,外層物件引用數為「0」
// 表示外層物件可以被「垃圾回收」了
// 然而,內層物件還被oa引用著,因此還是沒有被回收(個人註釋:這裡有一點閉包的意味)
oa = null; // 現在oa不引用內層物件了
// 內層物件也被垃圾回收
限制:循環引用
看下面程式碼:
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用o2
o2.a = o; // o2引用o
return “azerty”;
}
f();
// o o2兩個物件構成了循環引用
// 當函數執行完畢的時候,他們就被關在了f的作用域裡面,沒有外面的人可以使用他們
// 所以按理說,他們已經沒有存在價值了,需要被垃圾回收,釋放內存
// 然而,他們的引用計數都不為“0”
// 所以在這在種引用計數的機制下,他們沒有被回收
實際範例
在IE6,7版本的瀏覽器中,就是使用的引用計數機制。因此,下面的程式碼在IE6,7中可以穩穩地發生記憶體洩漏
var div = document.createElement("div");
div.onclick = function(){
doSomething();
}; // div的onclick屬性,會引用🎜function
// 然而這個function 反過來又引用了這個div,因為div在handler的作用域裡面。
// 造成上述循環引用,導致記憶體洩漏。標記清除演算法
這種演算法把“可以回收”定義成“物件不可達”,即無法存取。
這個演算法,會定義一個“根”,並且定期地從“根”出發,找出“根”下面的所有對象,看能不能從“根”找到一條路徑引用到這個對象。從不同的「根」出發,垃圾回收程序就可以區分所有物件是不是「不可達」的,當物件「不可達」時候,便被回收。
這種演算法比引用計數演算法好。因為 “一個物件的引用計數是0”可以推出“這個物件不可達”,逆命題則為假。也就是說這種演算法擴充了垃圾回收的範圍。
循環引用不再是困擾
在上面的循環引用例子中,當函數返回時,o 和o2都已經不再被任何人引用,也就是「不可達」了,便順理成章地被垃圾回收了。
限制:物件需要明確的「不可達」
雖然說是局限,然而這種情況在實際當中很少發生,因此很少有人關注這一點。