不同于C和C++等语言由开发者负责跟踪内存使用,JavaScript通过自动内存管理实现内存分配和闲置资源回收。释放不再使用的变量占用的内存称为垃圾回收。无论是什么程序语言,内存的生命周期基本是一致的:首先是分配所需内存,接着使用分配到的内存(读、写),最后在不需要时将其释放。
定义变量时完成内存分配:
let n = 123; // 给数值变量分配内存
function f(a){
return a + 1;
} // 给函数(可调用的对象)分配内存
有的函数调用结果是分配对象内存:
let d = new Date(); // 分配一个Date对象
let e = document.createElement('div'); // 分配一个DOM元素
主要的垃圾回收策略有标记清理和引用计数。
标记清理的核心概念是可达性(Reachability),即“可达”值是那些以某种方式可访问或可用的值。它们一定存储在内存中。常见的可达值包括:
这些值被称作根(roots)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
示例:
let user = {
name: "John"
}
在上例中,全局变量user
引用了对象{name: "John"}
。如果user
的值被重写了,这个引用就没了:
user = null
现在对象{name: "John"}
变成不可达的了,因为没有引用,就无法访问到它,因此垃圾回收器会将其回收,并释放被其占用的内存。
引用计数是一种不常用的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
示例:代码可视化示范
var o = {
a: {
b:2
} // 对象乙
}; // 对象甲
// 乙作为甲的属性被引用,甲被分配给变量o
var o2 = o; // o2变量是甲的引用
o = 1; // 现在,甲只有一个变量o2的引用,原始引用o已经没有
var oa = o2.a; // 变量oa引用乙
o2 = "yo"; // 现在甲已经零引用了,可以回收,但是它的属性a引用的对象乙还被oa引用,无法回收
oa = null; // 乙现在也是零引用了,它可以被垃圾回收了
引用计数的一个问题是循环引用,即对象A 有一个指针指向对象B,而对象B 也引用了对象A。比如:
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
}
在这种情况下,o
和o2
在函数结束后还会存在,因为它们的引用数不为0。如果函数多此调用,则会导致大量内存永远无法释放。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
https://zh.javascript.info/garbage-collection
马特·弗里斯比. JavaScript高级程序设计:第4版,P95-96.