前言
各位小伙伴八月快乐哈。
我是来自推啊前端团队的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-space,To-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..…