js垃圾回收机制

在js中创建一个变量时,会自动分配内存空间,当变量不再被使用时,垃圾回收机制会自动释放相应的内存空间。

如何判断一个变量不在被使用?方法有两种:

一、引用计数法:

引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,若是没有任何对象再指向它,那么垃圾回收器就会回收,其策略是跟踪记录每个变量值被使用的次数

  • 当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为 1

  • 如果同一个值又被赋给另一个变量,那么引用数加 1

  • 如果该变量的值被其他的值覆盖了,则引用次数减 1

  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

引用计数存存在一个致命的缺陷,当对象间存在循环引用时,引用次数始终不会为0,因此垃圾回收器不会释放它们。

function f() {
    var o1 = {};
    var o2 = {};
    o1.a = o2; // o1 引用 o2
    o2.a = o1; // o2 引用 o1
    return;
};
 
在 IE8 以及更早版本的 IE 中, BOM 和 DOM中的对象并不是原生的JS对象,而是使用C++以 COM对象的形式实现的,而 COM对象的垃圾收集机制采用的就是引用计数策略。因此,即使 IE 的 JavaScript引擎是使用标记清除策略来实现的,但JavaScript访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE8及以下版本中涉及 COM对象,就会存在循环引用的问题。下面这个简单的例子,展示了使用 COM对象导致的循环引用问题;
var element = document.getElementById("some_element");
var myObject = new Object{);
myObject. element = element;
element.someObject = myObject;
这个例子在一个DOM元素(element)与一个原生 JavaScript对象(myobject)之间创建了循环引用。而想要解决循环引用,需要将引用地址置为 null 来切断变量与之前引用值的关系,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
myObject.element = null;
element,SomeObject = null; 

二、标记清除法:

标记清除(Mark-Sweep),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法。

此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。

标记阶段,垃圾回收器会从根对象开始遍历(在js中,通常认定全局对象window做为根)。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。

清除阶段,垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。

 

在标记阶段,从根对象1可以访问到B,从B又可以访问到E,那么B和E都是可到达对象,同样的道理,F、G、J和K都是可到达对象。

在回收阶段,所有未标记为可到达的对象都会被垃圾回收器回收。

标记清除法会导致内存碎片化。由于空闲内存块是不连续的,容易出现很多空闲内存块,假设我们新建对象分配内存时需要大小为 size,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配(如下图)

那如何找到合适的块呢?我们可以采取下面三种分配策略

  • First-fit,找到大于等于 size 的块立即返回

  • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块

  • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回

 

这三种策略里面 Worst-fit 的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit 和 Best-fit 来说,考虑到分配的速度和效率 First-fit 是更为明智的选择,但即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

三、v8引擎的垃圾回收机制

Chrome 浏览器所使用的 V8 引擎采用的分代回收策略,该策略通过区分「临时」与「持久」对象;多回收「临时对象区」(young generation),少回收「持久对象区」(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。

「临时」与「持久」对象也被叫做作「新生代」与「老生代」对象。

 

1. 新生代的特点:

  • 通常把小的对象分配到新生代
  • 新生代的垃圾回收比较频繁
  • 通常存储容量在1~8M

2. 新生代-Scavenge算法

该算法将新生代分为两部分,一部分叫做from(对象区域),另一部分叫做to(空闲区域),新加入的对象首先存放在from区域;

from区域写满的时候,对from区域开始进行垃圾回收。首先对from区域的垃圾进行标记(红色代表标记为垃圾);

将存活的对象复制到to区域中,并且有序地排列起来,复制后的to区域就没有内存碎片了;

清空from区域;

from区域和to区域进行反转,也就是原来的from区域变为to区域,原来的to区域变成from区域。

Scavenge算法在时间效率上有着优异的表现,缺点是只能使用堆内存中的一半,如果存储容量过大,就会导致每次清理的时间过长,效率低,因此经过两次垃圾回收之后依然存活的对象会晋升为老生代对象,另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge 回收后,空闲区将翻转成对象区域,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。

 

3. 老生代的特点:

  • 对象占用空间大
  • 对象存活时间长


4. 老生代-标记整理法

  • 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象;
  • 整理:让所有存活的对象都向内存的一端移动

 

5. 何时执行垃圾回收?

由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。

为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法

 

 四、哪些情况容易引起内存泄漏

1. 全局变量

全局变量等同于在window上添加属性,因此在函数执行完毕,依旧能够访问到它,因此不能够被回收。

2. 闭包

3. 被遗忘的定时器或事件回调函数

当dom元素被移除时,因为是周期定时器的缘故,定时器回调函数始终没法被回收,这也致使了定时器会一直对数据serverData保持引用,好的作法是在不须要时中止定时器

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

另外在使用事件监听时,若是再也不须要监听记得移除监听事件

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

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

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

  

posted @ 2022-03-20 23:03  我是格鲁特  阅读(1800)  评论(0编辑  收藏  举报