理解v8的垃圾清理

目录

一、垃圾的产生

二、垃圾回收的步骤

1.标记

2.清理

3.整理

三、垃圾回收器

1.副垃圾回收器

2.主垃圾回收器

四、v8对垃圾回收的优化

1.并行回收

2.增量回收

3.并发回收

五、常见内存问题

1.内存泄漏

2.内存膨胀

3.频繁的垃圾回收


我们知道在v8中用堆和栈管理程序的存储,栈的垃圾的处理很简单,当函数执行完毕,对应的作用域中的数据将会被销毁也就是栈中的数据将会被销毁,程序的控制权将会交给上一层函数。一般所说的垃圾处理都是值对堆的垃圾的处理,先要了解怎样处理堆中的垃圾,我们需要知道,堆中的垃圾是如何产生的

一、垃圾的产生

我们都知道栈的存储结构是连续的,堆的存储结构是不连续的。这就决定了栈本身的存储空间不会很大,一般1M~8M。所以我们说js中的栈一般存储基本的类型,比如小整型,以及对象的地址,对于引用类型的实际存储位置是在堆中。对于堆中的垃圾的产生,见如下代码:

var temp = {
    name: "jyy",
    addr: "hebei"
}
temp = null;

代码中首先给temp赋值了一个对象,此时的存储结构是酱婶儿的:

理解v8的垃圾清理_第1张图片

 

 

当temp的值赋值给null时,此时的存储结构是酱婶儿的

理解v8的垃圾清理_第2张图片

可以看到,变量赋值为null,他和堆中对象的联系就关闭了,此时这个对象就成为了垃圾数据。

二、垃圾回收的步骤

垃圾回收拢共分三步:

标记、清理、整理

1.标记

标记是将空间中的对象标记为活动对象和非活动对象(垃圾数据),具体为:将GC Root作为初始存活的对象的集合,包括带不仅限于一下几种:

  1. 全局的 window 对象(位于每个 iframe 中);
  2. 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
  3. 存放栈上变量。

标记开始后会从GC Root开始遍历,若对象能够被GC Root遍历到,则标记为可访问的,否则为不可访问的。

2.清理

将标记为不可访问的数据从内存中清除

3.整理

清理完内存中的垃圾数据后,内存中势必会出现大量的不连续空间,称之为内存碎片,这下知道window的碎片整理中的碎片是怎么来的了吧(当然不是js干的。。。)。

理解v8的垃圾清理_第3张图片

 

因为如果内存碎片大量的存在,就会导致无法存储比较大的对象,就像这样:

理解v8的垃圾清理_第4张图片

 

 

 

如果进行了碎片整理,就会这样:

理解v8的垃圾清理_第5张图片

 

三、垃圾回收器

v8中的垃圾回收器分为主垃圾回收器( Major GC)和副垃圾回收器(Minor GC)。之所以使用了两个回收器,主要是收到代际假说(generational hypothesis)的影响,代际假说包含以下两点:

  1. 大部分的对象在内存中的存活时间很短 ————副垃圾回收器
  2. 不死的对象活得时间会更久 ————主垃圾回收器

在v8中堆划分为两个部分:新生代和老生代,新生代用于存放生存时间比较短的对象,老生代存放生存时间长或者是比较大的对象,新生代的容量比较小一般为1-8M,老生代的容量比较大。因此v8使用了两个不同的垃圾回收器,对新生代和老生代进行垃圾回收。

1.副垃圾回收器

副垃圾回收器用于对新生代进行垃圾回收。副垃圾回收器使用Scavenge算法,其核心是把新生代分为两个部分:空闲区和对象区。此时堆的结构如下图所示:

理解v8的垃圾清理_第6张图片

 

当有新加入的对象时,该对象会被存储到对象区,当对象区即将被写满时,就会副垃圾回收器执行一次垃圾回收。首先对对象区的对象进行标记,然后开始清理,遇到标记为活动对象的对象会被有序的放入空闲区,随后清空对象区,此时的空闲区就不会存在内存碎片,相当于一次性的完成了清理和整理的步骤。

理解v8的垃圾清理_第7张图片

 

然后之前的空闲区作为对象区,之前的对象区作为空闲区,准备下一轮的垃圾回收。

理解v8的垃圾清理_第8张图片

 

另外如果对象在副垃圾回收器中经历了两次的垃圾回收都没有被清理掉,那么这个对象就会被升级到老生代,被主垃圾回收器支配命运。垃圾就是垃圾到了哪里都是被别人决定命运。

2.主垃圾回收器

主垃圾回收器用于对老生代进行垃圾回收。老生代中的对象来自于两个部分:一个是对象比较大,直接放入老生代,另一个是从副垃圾回收器升级到老生代。所以,它的特点就是对象存储空间大,对象生存时间长。不同于副垃圾回收器的scavenge算法,主垃圾回收器使用的是标记-清除(mark-sweep)和标记-整理(mark-compact)用于垃圾清理和整理。之所以没有和副垃圾回收器使用相同的算法,是因为新生代存储的都是小的对象,复制花费的时间可以接受,但大对象的复制花费的时间比较长。

首先主垃圾回收器对老生代中的对象进行标记,然后使用标记-清理算法将标记为非活动对象的对象直接清除,如下图:

理解v8的垃圾清理_第9张图片

 

此时我们发现清理完毕后老生代同样会产生内存碎片,既然没有将大对象进行复制,那么它是怎么做的呢?

主垃圾回收器是使用标记-整理将标记的活动对象向一端移动,然后直接清理掉这一段之外的内存,如图:

理解v8的垃圾清理_第10张图片

 

四、v8对垃圾回收的优化

虽然v8的垃圾回收看似很高效,但是因为js是运行在主线程上的,那么一旦启动垃圾回收,就会导致程序完全停止下来,当垃圾回收完毕后再恢复执行。如下图所示:

理解v8的垃圾清理_第11张图片

 

可以看出这样的情况对于实际使用中完全是不可取的,想象一下,用户吃着火锅唱着歌,看着令人激动的电影,突然张麻子....啊不,是浏览器启动了垃圾回收机制,画面瞬间卡住不动,此时父母进入房间,想关关不掉,那么用户肯定会想,等一切沉挨落腚,这个浏览器势必打入冷宫。

v8当然不会忍心让我们这样尴尬,于是他加入了很多技术,总的来说这些技术是从两方面来解决垃圾回收的效率问题:

  1. 将一个完整的垃圾回收任务切分为多个小任务
  2. 将标记对象和移动对象等任务转移到后台线程进行

那么都是哪些新技术呢?

1.并行回收

既然执行一次完整的垃圾回收非常耗时,那么我可以引入多个辅助线程啊,如下图:

理解v8的垃圾清理_第12张图片

 

新生代的副垃圾回收器就是使用的并行回收,在回收时启动多个线程对垃圾进行清理,并将数据从对象区移动到空闲区。用毛爷爷一句话就是:人多力量大。

2.增量回收

并行回收固然好,但是如果只使用并行回收依旧还是会造成停顿,依然不能保证我们的屁股是完美的,此时就需要增量回收了。增量回收是将垃圾回收任务切分为多个小任务,然后将这些小任务穿插到主线程中的不同任务之间进行,如下所示:

理解v8的垃圾清理_第13张图片

 

看到这里,你肯定会有疑问,如果垃圾回收其中一个小任务暂停了,那么下次任务从何处开始?如果暂停期间被标记好的对象被修改了,那么垃圾回收器怎样纠正过来?

v8已经想到你的小心思了,他是酱解决的:

我们知道传统的标记是将对象划分为活动对象和非活动对象,假定活动对象对应于黑,标记之前所有的对象都为白,标记完成后,所有未被标记为黑的对象对被视为非活动对象,直接清理掉。那么我们标记完一段节点后,可能是这样的:

理解v8的垃圾清理_第14张图片

 

那么如果结束了这段标记任务后,继续执行js代码之后,下一段标记任务开始时,可能会出现下面的情况:

理解v8的垃圾清理_第15张图片

 

这种情况任谁也不能分辨出上一次的标记任务标记到了哪里。于是乎v8又引入了一个状态:灰色,这样的标记方式称为三色标记法,三种颜色代表的含义如下:

  1. 黑色表示这个节点被 GC Root 引用到了,而且该节点的子节点都已经标记完成了
  2. 灰色表示这个节点被 GC Root 引用到,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点;
  3. 白色表示这个节点没有被访问到,如果在本轮遍历结束时还是白色,那么这块数据就会被收回

ok,此时任务的开始结束点我们找到了,接下来我们需要解决js代码修改的问题了。例如如下代码:

window.a = Object()
window.a.b = Object()
window.a.b.c=Object() 

标记完成后的结果如下:

理解v8的垃圾清理_第16张图片

 

之后我又执行了如下的代码:

window.a.b = Object() //d

此时按道理结果如下:

理解v8的垃圾清理_第17张图片

 

这样的话d是白色的,而且这表路径已经标记完成,看来d势必是要被清理了。这种情况当然不会发生,因为v8加入了一个这样的约束条件:不能让黑色节点指向白色节点,如果发生了黑色的节点引用了白色的节点,那么这个被引用的白色节点会被强制转换为灰色。如上图,当第二次的标记任务开始时,d节点会被变为灰色,然后从d节点开始进行标记。至此,问题得以解决。

理解v8的垃圾清理_第18张图片

 

3.并发回收

事实上,此时还谈不上完美,因为js主线程的停顿依旧没有解决,屁股还是不能得到保障。

所谓并发回收,是指主线程在执行js的过程中,垃圾回收的工作完全交给辅助线程在后台执行,如下图所示

理解v8的垃圾清理_第19张图片

 

当然这样的做的问题也是存在的,比如在主线程执行js的过程中,堆中的内容随时会修改,那么辅助线程的工作很可能白做了,这个解决问题太过麻烦,没有找到解决方法,有兴趣的可以去看v8源码。

并发的回收看起来已经完美的解决了所有的问题,但是前面的两种技术v8并没有抛弃掉,而是在主垃圾回收器中将三者结合来实现垃圾回收,其具体的工作方式如下:

理解v8的垃圾清理_第20张图片

 

      可以看到,主垃圾回收器使用并发标记,并行整理,使用增量标记,将清理任务穿插到js任务中。

看上去有点乱,可能表格会更直观些:

理解v8的垃圾清理_第21张图片

 

五、常见内存问题

总的来说,内存问题分为以下三类:

  1. 内存泄漏,会导致页面的性能越来越差
  2. 内存膨胀,导致页面性能一直很差
  3. 频繁垃圾回收,导致页面延迟或暂停

1.内存泄漏

造成内存泄露的主要原因是不再需要的数据依然被对象引用,常见的内存泄漏的情况如下:

首先是全局变量的问题,例如:

function demo(){
    arr_city = new Array(100000);
}
demo();

我们都知道这段代码的结果是arr_city会保存在全局对象中,而事实上是我们很有可能不需要再次使用它,如果这样的情况越来越多就会有可能导致内存泄漏。这也是为何不提倡设置全局变量,当然为了防止命名冲突也是重要的一点。那么解决这个问题的方式很简单就是尽量不使用全局变量,或者是用完之后清理掉。

第二个可能造成内存泄露的问题就是滥用闭包,例如下代码:

function foo(){  
    var temp_object = new Object()
    temp_object.x = 1
    temp_object.y = 2
    temp_object.array = new Array(200000)
    /**
    *   使用temp_object
    */
    return function(){
        console.log(temp_object.x);
    }
}

在这段代码中,我们没有仅仅在内部的匿名函数中使用了temp_object的x属性,似乎即便x被长期存储在内存中也不会有什么太大影响,然而事实并非如此:

理解v8的垃圾清理_第22张图片

 

我们在打断点后,返现scope中包含了closure,这是没问题的,但是问题在于,这其中将闭包中的temp_object都进行了保存,不仅仅包括x,要知道array中元素的个数有200000!这种情况必然很容易出现内存泄漏。那么处理这个情况的好的方式是将内部的函数中使用的外部变量设置为局部变量,如下:


function foo(){  
    var temp_object = new Object()
    temp_object.x = 1
    temp_object.y = 2
    temp_object.array = new Array(200000)
    /**
    *   使用temp_object
    */
   let closure = temp_object.x
    return function(){
        console.log(closure);
    }
}

第三种情况是由于js引用了DOM节点而造成的泄露问题,只有在DOM树和js代码都不引用DOM节点,这个节点才会被垃圾回收,若节点已经从DOM树中移除,但是js仍然引用他,我们就把这个节点称为:detached。例如下代码:

var ul = document.createElement("ul");
for(var i = 0; i < 10; i++){
    var li = document.createElement("li");
    li.innerHTML = i;
    ul.appendChild(li);
}
document.body.appendChild(ul);
document.body.removeChild(ul);
console.log(ul);

代码中创建了一个ul,在ul中创建了十个li,并且将ul放入body中,然后移除掉ul,然后打印ul,这是发现如下的打印结果:

理解v8的垃圾清理_第23张图片

 

虽然我们已经将ul节点在DOM树中移除,但是此时ul中依旧保存着DOM的节点信息,倘若ul中的dom节点的信息很多,那么很容易造成内存溢出。解决这个问题的方法就是在移除DOM节点后,将js中引用DOM节点的变量设置为null。

2.内存膨胀

内存膨胀和内存泄漏有些相似,不同点是内存膨胀是因为一次性加载了大量的资源,内存会快速达到一个峰值,其后会平稳。如下图所示:

理解v8的垃圾清理_第24张图片

 

解决的办法就是科学的管理内存,避免一次性加载大量资源。

3.频繁的垃圾回收

这个问题是因为频繁的使用大的临时变量,导致了新生代很快被装满,从而频繁的触发新生代的垃圾回收机制,这会导致页面的卡顿。解决方法就是可以将临时变量设置为全局变量

你可能感兴趣的:(javascript基础)