# 内存管理

# 内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放、归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。

js的内存生命周期:

  1. 定义变量时就完成了内存分配
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
var o = {
  a: 1,
  b: null
}; // 给对象及其包含的值分配内存
function f(a){
  return a + 2;
} // 给函数(可调用的对象)分配内存
Copied!
  1. 使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
  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对象已经没有引用,很快会被回收
Copied!
# 循环引用(上面垃圾回收的限制)
function fun(){
  var obj = {};
  var obj2 = {};
  obj.a = obj2; // obj 引用 obj2
  obj2.a = obj; // obj2 引用 obj
  return "azerty";
}
fun();
Copied!

两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

# 标记-清除算法

现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

# 内存泄漏

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏。

# 内存泄漏的识别方法

如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。

  1. 浏览器方法
    1. 打开开发者工具,
    2. 在顶部的Capture字段里面勾选 Memory 选择 Timeline 面板
    3. 点击左上角的录制按钮。
    4. 在页面上进行各种操作,模拟用户的使用情况。
    5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。 反之,就是内存泄漏了。

  1. 命令行方法 命令行可以使用 Node 提供的 process.memoryUsage 方法。

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,

console.log(process.memoryUsage());
//  rss:        所有内存占用,包括指令区和堆栈。
//  heapTotal:     "堆"占用的内存,包括用到的和没用到的。
//  heapUsed:    用到的堆的部分。
//  external:     V8 引擎内部的 C++ 对象占用的内存。

// 判断内存泄漏,以heapUsed字段为准。
Copied!

# 常见内存泄漏

如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。

对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关。

  1. 绝对不要定义全局变量
    JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是 window 。为了防止这种意外,可以使用严格模式来阻止
  2. 手工解除变量引用
    一个变量已经确切是不再需要了,那么就可以手工解除变量引用,以使其被回收。
  3. 闭包
  4. DOM外引用