内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
分配所需要的内存
在js中,值的初始化以及一些函数调用自动完成内存分配使用分配到的内存
对分配的内存进行读取与写入的过程当内存不再需要时释放
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。
垃圾回收
js具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
在编写js程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。
这种垃圾收集机制其实非常简单:找出那些不再继续使用的变量,然后释放其占用的内存。
为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
下面来分析一下函数中局部变量的正常生命周期:
- 局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆内存)上分配相应的空间,以便存储它们的值。
- 在函数中使用这些变量,直到函数执行结束。
- 此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。
在这种情况下,很容易判断变量是否还有存在的必要。
但并非所有情况下都这么容易就能得出结论。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。
对于标记无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略:
标记清除
js中最常用的垃圾收集方式是标记清除。
JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象…对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进。
引用计数
在内存管理环境中,对象 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 的引用次数都至少为1,所以他们不会被回收。
要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null;
obj2 = null;
而使用标记清除方法,循环引用不再是问题了。
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。
内存泄漏
内存泄漏是指计算机可用的内存越来越少,主要是因为程序不能释放那些不再使用的内存。
循环引用
就是上面引用计数里面的例子那样。
一旦数据不再使用,最好通过将其值设为 null 来释放其引用,这个方法被称为“解除引用”。
无意的全局变量
function foo(arg) {
bar = "";
}
foo();
上面的代码就无意中声明了一个全局变量,会得到 window 的引用,bar 实际上是 window.bar,它的作用域在 window 上,所以 foo 函数执行结束后,bar 也不会被内存收回。
另外一种无意的全局变量的情况是:
function foo() {
this.bar = "";
}
被遗忘的计时器和回调函数
在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。那什么才叫结束呢?就是调用了 clearInterval。如果回调函数内没有做什么事情,并且也没有被 clear 掉的话,就会造成内存泄漏。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。
所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout。