JavaScript垃圾回收和内存管理

1,022 阅读6分钟

前言

各位小伙伴八月快乐哈。 我是来自推啊前端团队的D同学。 今天跟大家简要分享一下JavaScript的垃圾回收机制和内存管理策略。

在学习闭包的时候,我们会经常提及比较重要的缺点之一,不当的闭包(在IE9之前)会引起内存泄漏

内存泄漏是指:程序中已动态分配的堆内存由于某种原因,无法释放而造成的内存资源的浪费。

为什么在IE9之前会发生这种状况呢?原因就是在IE9之前垃圾回收机制采用的方法是引用计数法

JavaScript两种垃圾回收(GC)的机制

1. 引用计数法

​ 该算法核心在于,跟踪记录每个对象被引用的次数。每当存在一个变量,且该变量的值为对某个对象的引用时,该对象的引用+1。相反,如果每存在一个引用该对象的变量发生更改,从而不在引用该对象时,该对象的引用-1。

​ 当对象的引用值为0时,说明该对象不被任何变量所指向,即没有变量可以访问它。因此,就可以将该对象所在内存空间释放回收。

​ 在IE9之前采用该方法,会出现类似以下的内存泄漏场景

function bar(){
    let a = {}
    let b = {}
    a.foo = b
    b.foo = a
}
bar()

这个场景中,由于a和b的属性存在相互引用,因此当函数执行完,由于a和b引用计数均不为零,因此ab所在的内存空间,将不会被回收。

2. 标志清除法

​ 该方法主要可分为两个阶段

  • 标志阶段

    标记阶段就是找到可访问对象的一个过程;垃圾回收是从一组对象的指针(objects pointers)开始的,我们将其称之为根集(root set),这其中包括了执行栈全局对象;然后垃圾回收器会跟踪每一个指向 JavaScript 对象的指针,并将对象标记为可访问的,同时跟踪对象中每一个属性的指针并标记为可访问的,这个过程会递归的进行,直至所有节点没有可遍历的路径。

  • 清除阶段

    标志阶段结束后,未被打上标志的对象,说明从根节点无法访问它,垃圾回收器就会回收该内存。

标志清除法

如图所示,当从根节点开始深度优先遍历时,遍历不到的对象,即代表着根节点无法访问。

图中红色标志的即为未被标志的对象,当遍历结束后,会被垃圾回收器回收。

可以看出标志清除法可以解决对象相互引用导致内存泄漏的问题。

目前,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾回收策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。

V8引擎的内存回收策略

分代内存

在V8引擎的内存结构中,堆内存分为两类进行处理。新生代内存和老生代内存。

  • 新生代内存

新生代内存是指临时分配内存,存活时间短。

新生代内存可分为两个区域From-spaceTo-space

其中from区域还可细分为nursery子代和intermediate子代

  • 老生代内存

老生代内存是常驻内存,存活时间长。

整体内存分配如图 内存分配

Scavenge算法

新生代内存的垃圾回收算法称为Scavenge算法

过程

其中From部分为正在使用的内存,To为当前闲置内存。

初始化

  • 当执行垃圾回收时,V8引擎会将From内存中对象检查一遍,然后把存活的对象复制到To内存中,非存活的对象直接回收。

  • 当复制完成后,清空From内存,然后From和To角色对调。该过程称为一次Scavenge算法。

对调

特性

可能有人会说,为什么不直接内存分配全部给From,然后对非存活对象进行内存回收不就行了?

答案是否定的,由于堆内存是连续分配的,因此进行垃圾回收之后就有可能会出现不连续的内存碎片,这样零散的内存空间会导致大一点的对象,没办法进行空间分配。

缺点:Scavenge算法的垃圾回收过程主要就是将存活对象在From空间和To空间之间进行复制,同时完成两个空间之间的角色互换,因此该算法的缺点也比较明显,浪费了一半的内存用于复制。

优点:Scavenge算法是一种典型的牺牲空间换取时间的算法,对于老生代内存来说,可能会存储大量对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现可观,所以还是比较适合这种算法。

晋升

如果新生代中的一个变量经过多次垃圾回收后,依旧存活。该对象就会被认为是一个声明周期较长的对象,被放入老生代内存中,对象从新生代转移到老生代的过程,称为晋升

对象晋升的条件有两个:

  • 已经经历过一次Scavenge算法(即在Form空间中的intermediate子代区域中的对象)
  • To(当前闲置)空间内存占用超过25%

晋升

老生代垃圾回收

老生代垃圾回收,即采用上述标志清除法(Mark-Sweep),但是标志清除法会产生上述提及的内存碎片问题。

为了解决这个问题,在标志清除法过程中还有着Mark-Compact机制,即在进行一次标志清除之后,将所有存活的对象内存空间,向前移动。

老生代

由于是移动对象,它的执行速度不可能很快,事实上也是整个垃圾回收过程中最耗时间的部分

增量标记

由于JavaScript的单线程机制,在V8引擎进行垃圾回收时,会阻塞应用层业务逻辑的执行。该行为称为"全停顿“。

长时间的”全停顿“垃圾回收,用户体验感变得非常糟糕,同时会严重影响性能。

因此为了避免这种问题。2011年起,v8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6。

增量标记,即将一次全停顿的过程,拆分为多个’小步‘,每执行完’一小步‘就让JavaScript应用层的逻辑执行一小会。垃圾回收和应用逻辑交替执行,直至完成标记。

增量标记

作者:D
链接:juejin.cn/post/6992..…