# 内存管理
# 内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放、归还
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。
js的内存生命周期:
- 定义变量时就完成了内存分配
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
var o = {
a: 1,
b: null
}; // 给对象及其包含的值分配内存
function f(a){
return a + 2;
} // 给函数(可调用的对象)分配内存
- 使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
- 而内存的释放而依赖GC机制(高级语言解释器嵌入的“垃圾回收器”)。
# 垃圾回收
# 引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
# 引用计数垃圾收集(计数算法)
把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
无法处理循环引用
// 创建一个对象person,他有两个指向属性age和name的引用
var person = {
age: 12,
name: 'aaaa'
};
person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收
var p = person;
person = 1; //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收
p = null; //原person对象已经没有引用,很快会被回收
# 循环引用(上面垃圾回收的限制)
function fun(){
var obj = {};
var obj2 = {};
obj.a = obj2; // obj 引用 obj2
obj2.a = obj; // obj2 引用 obj
return "azerty";
}
fun();
两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
# 标记-清除算法
现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
# 内存泄漏
对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏。
# 内存泄漏的识别方法
如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。
- 浏览器方法
- 打开开发者工具,
- 在顶部的Capture字段里面勾选 Memory 选择 Timeline 面板
- 点击左上角的录制按钮。
- 在页面上进行各种操作,模拟用户的使用情况。
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。 反之,就是内存泄漏了。
- 命令行方法
命令行可以使用 Node 提供的
process.memoryUsage
方法。
process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,
console.log(process.memoryUsage());
// rss: 所有内存占用,包括指令区和堆栈。
// heapTotal: "堆"占用的内存,包括用到的和没用到的。
// heapUsed: 用到的堆的部分。
// external: V8 引擎内部的 C++ 对象占用的内存。
// 判断内存泄漏,以heapUsed字段为准。
# 常见内存泄漏
如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。
对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关。
- 绝对不要定义全局变量
JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是 window 。为了防止这种意外,可以使用严格模式来阻止 - 手工解除变量引用
一个变量已经确切是不再需要了,那么就可以手工解除变量引用,以使其被回收。 - 闭包
- DOM外引用