【JavaScript】内存管理和垃圾回收机制

42 阅读4分钟

内存管理

C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。

而 JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。

整个过程为:分配内存 -> 使用内存 -> 清理内存。

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏。

JavaScript 在定义变量或者函数调用时就完成了内存分配:

var n = 123; // 给数值变量分配内存

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";
  },
  false,
);

var d = new Date(); // 分配一个 Date 对象

var e = document.createElement("div"); // 分配一个 DOM 元素

垃圾回收机制

js 具有自动垃圾回收机制(GC)。垃圾回收器会定期(周期性)找出不在继续使用的变量,然后释放其内存。

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。

局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

垃圾收集器跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。

用于标记的无用变量的策路可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。

1. 引用计数

var o = {
  a: {
    b: 2,
  },
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集

var o2 = o; // o2 变量是第二个对“这个对象”的引用

o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有

var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收

oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了

存在的问题:循环引用

引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f() {
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o
  return "azerty";
}

f();

2. 标记清除

过程分为:标记 -> 清除。

function test() {
    var a = 10  // 被标记 进入环境
    var b = 20  // 被标记 进入环境
}
test()  // 执行完毕 之后 a b 又被标记 离开环境

垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。

然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。