八股文不用背-浏览器的GC-垃圾回收机制

467 阅读4分钟

本文章除了引导部分,其他全部来自一篇文章

链接

背景

某个函数在运行时创建了一些变量,并在内存中给这些变量分配了空间,当函数运行完之后,显然需要将这些变量占用的空间给释放掉,而这个操作就是GC(garbage collect),垃圾回收。

举个栗子

let name = 1
let obj = {name:2}
function func1(){
    let arr1 = [1]
}
function func2(){
    let arr2 = [2]
}
func1()
func2()

GC执行时,js线程会被阻塞

js代码是运行在V8引擎中的,全局js函数在运行时分别会创建全局上下文函数上下文,并都会压进一个执行栈中,非引用类的数据就放在上下文,引用类的数据都会在中分配内存空间。
比如js运行到了let arr2 = [2]这行代码,但是func2还没结束时,V8触发了第一次GC,那么此刻的状态就是:

image.png

GC过程:标记清除法

V8的GC过程分为了三步:

  1. 判断堆里哪些数据是被引用了的
  2. 遍历堆,将没被引用的数据清除掉
  3. 整理内存(看情况)

第一步:判断堆里哪些数据是被引用了的

V8引擎会将遍历全局对象(window)、dom树,运行栈中的上下文,然后又引用堆里的某个数据,那么就会将这个数据打上标记。比如上图的[2]和{name:2}就分别被func2的函数上下文和全局上下文给引用到了,所以会被打上标记;而[1]没有人引用,所以没有标记。

第二步:遍历堆,将没被引用的数据清除掉

遍历堆,将堆中没被标记的数据直接销毁,销毁掉[1]

第三步: 整理内存

销毁掉[1]之后

image.png

清除完之后,堆这个数组中的某几位就会空缺,我们需要整理一下,用后面的补上这个空位

通过这种方式,V8就能将堆中没有被引用的数据给清理掉

优化

回忆一下标记清除的三部曲:

  1. 判断堆里哪些数据是被引用了的
  2. 遍历堆,将没被引用的数据清除掉
  3. 整理内存 有没有一种可能,我们能优化GC过程,减少GC时间,这样就能减少阻塞Js线程的时间。
    第一步和第二步是没办法了,看看第三步。
    如果我们能将堆中的数据分成常驻和短驻的两类,那么每次GC时,常驻那部分就大概率不需要整理内存,这样就能节省时间。

而且对于动态语言,刚被分配内存的数据,往往马上就被清除了,比如最初的例子的func1中,arr1刚被分配内存,然后马上就被GC掉了,即js中函数的上下文(非闭包)大概率刚进入运行栈就执行完被弹出了。

根据这个特性,V8的GC机制将堆分为了两个区,新生代旧生代,而新生代被一分为二,其中一半作为缓冲,这个缓冲后面解释。

image.png

回到最初的例子:js执行到func2中的let arr2 = [2],GC执行到第二步时

image.png

新生代的第一个堆中的{name:2}和[2]被标记次数都是1,所以在清除阶段,会被移动到缓冲堆,清除[1]。 清除完之后,完全清空新生代的第一个堆,然后V8会将新生代的第一个堆和第二堆的身份互换,如下图:

image.png

GC完毕,继续执行js线程,当执行完func2之后,V8触发了第二次GC的话,此刻运行栈和堆的状态如下:

image.png

GC:

  1. 打标记:因为第一个GC时{name:2}标记为1,而这次GC{name:2}标记次数则为2;[2]没有标记,因为没有被引用
  2. 清除:{name:2}的标记为2,所以直接越过缓冲堆,直接到旧生代,[2]没标记,被清除
  3. 整理内存:旧生代不需要,新生代也不需要。

为什么要用缓冲堆

  1. 如果新生堆中的数据只要在一次GC下存活就放到旧生代的话,那么旧生代里的数据,大概率会在下一次GC时被删除,产生内存碎片,这就需要内存整理了(只是概率大小的问题,多一层缓存,就能很大程度减少旧生代内存整理的频率)
  2. 新生代多了个缓冲堆后,发现没,每次GC,我们都是直接清空第一个堆,第二个堆不需要干什么,然后两个互换,这样我们就不需要对新生代的两个堆做内存整理了,而只需要做复制第一个堆的数据到第二个堆。

GC触发的时机

  1. 定时
  2. 新生代、新生代缓冲区、旧生代的比例达到阈值

看到这里的看官,麻烦点个赞赞

xdm,有问题,评论区见