2. JavaScript 回收机制

293 阅读6分钟

由于字符串、对象和数组没有固定大小,所有当它们的大小已知时,才能对它们进行动态的存储分配。JavaScript 程序每次创建字符串、数组和对象时,解释器都必须分配内存来存储那个实体。只有像这样动态地分配了内存,最终都要释放这些内存以便它们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

JavaScript 回收机制的就是:找出不再使用的变量,然后释放其占用的内存,但是这个过程不是实时的,因为开销较大,所以垃圾回收机制是按照固定的时间间隔周期性的执行

标记清除

标记清除是 JavaScript 最常用的垃圾回收方式。

当变量进入执行环境时,就标记这个变量”进入环境“。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。

当变量离开环境时,则将其标记为“离开环境”。

垃圾收集器在运行的时候会给存储在内存中的所有的变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量上的标记。而在此之后再被加上标记的变量将视为准备删除的变量,原因是环境中的变量无法访问到这些变量了。

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

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用计数

所谓“引用计数”是指语言引擎有一张“引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

81223741

上图中,左下角的两个值,没有任何引用,所以可以释放。

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
console.log('浪里行舟');

上面代码中,【1,2,3,4】是一个值,会占用内存。变量 arr 是仅有的对这个值的引用,因此引用次数为 1,尽管后面的代码没有用到 arr,但是它还是会持续占用内存。

到了下面的代码,arr 又取得了另一个值,数组【1,2,3,4】的引用 -1 归零,它所占用的内存空间才被释放。

而引用计数还有最大的问题:循环引用:

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行完毕,返回值是 undefined,整个函数的变量都应该被回收,但是如果按照引用计数的方法,obj1 和 obj2 的引用都不为 0,因此它们都无法被自动回收,只有通过手动设置为空:obj1 = null;obj2 = null;它们才能被回收。

内存泄漏

虽然 JavaScript 会自动进行垃圾回收,但如果我们的写法不当,会让变量一直处于“进入环境”的状态,导致垃圾回收机制无法回收。(现在的 JavaScript 多用 标记清除 的垃圾回收方式)

意外的全局变量

我们在函数内定义变量时,如果前面没有加 var,let,const,那么我们定义的实际上是全局变量:

function fooarg{
	bar = “this is a hidden global variable”;
}

81223742

还有,当我们直接调用函数时,函数的 this 是指向 window 的。这时也可能通过 this 创建了全局变量。

function foo(){
	this.variable = “potential accidental global”;
}
foo();  // foo 直接调用,this 指向的 window ,通过 this 创建了全局变量 variable:

81223743

如果我们启用严格模式(在 JavaScript 文件头部加上“use strict”),则不会发生这种错误。

被遗忘的计时器或回调函数

var someResource = getDate();
setInterval(function(){
	var node = document.getElementById('Node');
	if(node){
		node.innerHTML = JSON.stringify(someResource);
	}
},1000);

这样的代码很常见,如果 id 为 Node 的元素从 DOM 中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource 的引用,定时器外面的 someResource 也不会被释放。

闭包

闭包我们经常使用,闭包可以维持函数内的局部变量,使其不得释放。

我们常用的闭包:

function bindEvent(){
  var obj=document.createElement('xxx')
  obj.onclick=function(){
    // Even if it is a empty function
  }
}

这里函数内部定义了函数,就形成了闭包。

解决方法:

// 将事件处理函数定义在外面
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = onclickHandler
}

// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = function() {
    // Even if it is a empty function
  }
  obj = null
}

垃圾回收的使用场景优化

数组 array 优化

我们一般用 arr = [] 清空数组,但是这种方法固然释放了原来的数组,但是却又创建了一个新的空的对象,并且将原来的数组对象变成了一小片内存垃圾!

如果将 arr.length = 0,这种方式清空数组,就可以减少内存垃圾的产生。

const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0  // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。

对象尽量复用

对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。

var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
  // var t = {};// 每次循环都会创建一个新对象。
  t.age = 19
  t.name = '123'
  t.index = i
  console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。

在循环中的函数表达式,能复用最好放到循环外面

// 在循环中最好也别使用函数表达式。
for (var k = 0; k < 10; k++) {
  var t = function(a) {
    // 创建了10次  函数对象。
    console.log(a)
  }
  t(k)
}

改为:

// 推荐用法
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

查看原文