JavaScript中的内存回收机制

JS的内存回收

在js中,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。

以全局变量和局部变量来说,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。

而对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以尽量少使用全局变量。

垃圾回收的两种方式

1. 引用计数

引用计数的判断原理很简单,语言引擎有一张"引用表",保存了内存里面所有资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。举个例子:

// 创建一个对象,由变量o指向这个对象的两个属性
var o = {
    name: 'hello',
    handsome: true
};
// name虽然设置为了null,但o依旧有name属性的引用
o.name = null;
var s = o;
// 我们修改并释放了o对于对象的引用,但变量s依旧存在引用
o = null;
// 变量s也不再引用,对象很快会被垃圾回收器释放
s = null;

引用计数存在一个很大的问题,就是对象间的循环引用,比如如下代码中,对象o1与o2相互引用,即便函数执行完毕,垃圾回收器通过引用计数也无法释放它们。

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

2. 标记清除

标记清除的概念也好理解,从根部出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它,这个过程大致分为三步:

  • 垃圾回收器创建roots列表,roots通常是代码中保留引用的全局变量,在js中,我们一般认定全局对象window作为root,也就是所谓的根部。
  • 从根部出发检查所有 的roots,所有的children也会被递归检查,能从root到达的都会被标记为active。
  • 未被标记为active的数据被认定为不再需要,垃圾回收器开始释放它们。

当一个对象零引用时,我们从根部一定无法到达;但反过来,从根部无法到达的不一定是严格意义上的零引用,比如循环引用,所以标记清除要更优于引用计数。

(自2012年起,所有现代浏览器都使用了标记清除垃圾回收算法,但老版本的IE6除外。)

如何避免内存泄漏

既然已经知道了垃圾回收的原理,那么如何避免创建无法回收的对象,以至造成内存泄漏的尴尬呢?下面说说常见的四种js内存泄漏。

1. 全局变量

尽可能少的去创建全局变量是js开发者的常识,但如下两种方式还是会意外的创建全局变量

第一种是在函数中声明变量未使用var:

function fn() {
    a = 1;
};
fn();
window.a //1

在js中未使用任何修饰符声明变量,该变量默认提升为全局变量 。

第二种是在非严格模式下在函数体内通过this来创建变量:

function fn() {
    this.a = 1;
};
fn();
window.a //1

当直接调用函数fn时,等同于window.fn(),在非严格模式下函数体内的this会指向window,所以本质上还是创建了一个全局变量。 

有时候我们无法避免使用全局变量,那么记得在使用完毕后手动释放它们,例如让变量指向null。

2. 被遗忘的定时器或回调函数

var serverData = loadData();
setInterval(function () {
    var renderer = document.getElementById('renderer');
    if (renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 3000);

在上述代码中,当dom元素renderer被移除时,由于是周期定时器的缘故,定时器回调函数始终无法被回收,这也导致了定时器会一直对数据serverData保持引用,好的做法是在不需要时停止定时器。

在例如我们在使用事件监听时,如果不再需要监听记得移除监听事件。

var element = document.getElementById('button');

function onclick(event) {
    element.innerHTML = 'text';
};

element.addEventListener('click', onclick);
// 移除监听
element.removeEventListener('click', onclick);

3. 闭包

闭包在js开发中是极其常见的,我们来看个例子:

var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        //unused未执行,但一直保持对theThing的引用
        if (originalThing)
            console.log("hi");
    };
    //创建一个新对象
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log("message");
        }
    };
};

setInterval(replaceThing, 1000);

定时器每次调用replaceThing,theThing都会获得一个包含数组longStr与闭包someMethod的新对象。

闭包unused保持着对象originalThing的引用,因为theThing赋值的缘故,也保持了对theThing的引用。

虽然unused没执行,但引用关系会导致originalThing一直无法被回收,那么theThing也一样。

正确做法是在replaceThing 最后添加originalThing  = null;

所以我们常说,对于闭包中的变量,在不需要时一定记得手动释放。

4. DOM的引用

操作dom总是被认为是不好的,但一定得操作,我们的习惯是通过一个变量来存储它,这样就可以反复使用了,但这也会造成一个问题,dom会被引用2次。

var elements = document.getElementById('button')

function doStuff() {
    elements.innerHTML = 'hello aaa';
};
// 清除引用
elements = null;
document.body.removeChild(document.getElementById('button'));

在上述代码中,一次引用是基于dom树的引用,第二是变量elements的引用,当我们不需要这个dom时,都做两次清除操作。 

补充

不管是什么程序语言,内存的声明周期都满足以下三个阶段:

  • 分配你需要的内存空间
  • 使用分配到的内存(读、写)
  • 不需要时将其释放或归还

JS的内存空间 

JavaScript内存空间分为:队列

存放变量,基本类型数据与指向复杂类型数据的引用指针;

存放复杂类型数据;

池(一般也会归类为栈中)又称为常量池,用于存放常量;

队列在任务队列也会使用。

1. 栈空间

具备FILO(first in last out)先进后出的特性,较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。

在js中数据类型一般分类基本数据类型(Number Boolean Null Undefined String Symbol)与引用数据类型(Object Array Function),其中栈一般用于存放基本类型数据,例如以下代码在栈内存中分布:

var a = 1;
var b = a;
a = 2;

可以看到基本类型数据的变量名与值都存放在栈内存中,当我们将变量a复制给b时,栈会新开内存用于存放变量b,且当我们修改变量a时对变量b不会造成任何影响,因为a与b是互不相关的两份数据。 

2. 堆空间 

是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍。

在js中堆内存一般用于存储引用类型的数据,需要注意的是由于引用类型的数据一般可以拓展,数据大小可变,所以存放在堆内存中;

但对引用类型数据的引用地址是固定的,所以地址指向还是会存放在栈内存中。

我们通过内存图来模拟以下代码:

var a = [1,2,3];
var b = a;
a.push(4);

 

当我们创建数组a时,栈内存中只保存了变量a与指向堆内存中数组的地址指针,而当我们将a复制给变量b时,其实只是复制了一份地址指针,两者还是指向同一数组,无论谁修改,都会影响彼此。

这便是我们熟知的浅拷贝,若想对浅拷贝与深拷贝有更深了解,可以参考:让你彻底理解浅拷贝和深拷贝的区别_小小1001的博客-CSDN博客_深拷贝和浅拷贝的区别

 3. 队列

队列具有FIFO(First In First Out)先进先出的特性,与栈内存不同的是,栈内存只存在一个出口用于数据进栈出栈;而队列有一个入口与一个出口,理解队列一个较为实际的例子就像我们排队取餐,先排队的永远能先取到餐。

在js中使用队列较为突出的就是js执行机制中的event loop事件循环,如果大家对于js事件执行机制有兴趣,可以参考:JavaScript 执行机制浅谈_火星飞鸟的博客-CSDN博客

本篇文章参考:javascript的内存(垃圾)回收机制

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值