垃圾回收
# 垃圾回收策略
解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关 的值已经不在上下文里了,因此它在下次垃圾回收时会被回收
# 引用计数
这种方式常常会引起内存泄漏,低版本的 IE 使用这种方式。
# 工作原理
跟踪记录每个值被引用的次数。如果存在循环引用会导致内存无法释放
let a = {}
let b = {}
a.prop = b
b.prop = a
2
3
4
# 标记清除
JS 中最常见的垃圾回收方式是标记清除。
# 工作原理
当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
# 工作流程:
垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
去掉 环境中的变量以及被环境中的变量引用的变量的 标记。
再被加上标记的会被视为准备删除的变量。
垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
# V8 中内存分类
在讲内存分配之前,先了解一下弱分代假说,V8 的垃圾回收主要建立在这个假说之上。
概念:
- 绝大部分的对象生命周期都很短,即存活时间很短
- 生命周期很长的对象,基本都是常驻对象
基于以上两个概念,将内存分为新生代(new space)
与老生代(old space)
两个区域。划重点,记一下。
# 新生代垃圾回收
新生代(32 位系统分配 16M 的内存空间,64 位系统翻倍 32M,不同浏览器可能不同,但是应该差不了多少)。
新生代对应存活时间很短的假说概念,这个空间的操作,非常频繁,绝大多数对象在这里经历一次生死轮回,基本消亡,没消亡的会晋升至老生代内。
新生代算法为 Scavenge 算法,典型牺牲空间换时间,怎么说呢?
首先他将新生代分为两个相等的半空间( semispace ) from space 与 to space,来看看这个败家玩意,是怎么操作的,他使用宽度优先算法,是宽度优先。两个空间,同一时间内,只会有一个空间在工作( from space ),另一个在休息( to space )。
首先,V8 引擎中的垃圾回收器检测到 from space 空间快达到上限了,此时要进行一次垃圾回收了
然后,从根部开始遍历,不可达对象(即无法遍历到的对象)将会被标记,并且复制未被标记的对象,放到 to space 中
最后,清除 from space 中的数据,同时将 from space 置为空闲状态,即变成 to space,相应的 to space 变成 from space,俗称翻转
当然优秀的 V8 是不可能容忍,一个对象来回的在 form space 和 to space 中蹦跶的,(晋升条件 ①)当经历一次 form => to 翻转之后,发现某些未被标记的对象居然还在,会直接扔到老生代里面去。
除了上面一种情况,还有一个情况也会晋级,(晋升条件 ②)当一个对象,在被复制的时候,大于 to space 空间的 25% 的时候,也会晋级了,这种自带背景的选手,那是不敢动的,直接晋级到老生代。
# 老生代垃圾回收
老生代( 32 位操作系统分配大约 700M 内存空间,64 位翻倍 1.4G,一样,每个浏览器可能会有差异,但是差不了多少)。
老生代比起新生代可是要复杂的多,所谓能者多劳,空间大了,责任就大了,老生代可以分为以下几个区域:
- old object space 即大家口中的老生代,不是全部老生代,这里的对象大部分是由新生代晋升而来
- large object space 大对象存储区域,其他区域无法存储下的对象会被放在这里,基本是超过 1M 的对象,这种对象不会在新生代对象中分配,直接存放到这里,当然了,这么大的数据,复制成本很高,基本就是在这里等待命运的降临不可能接受仅仅是知其然,而不知其所以然
- Map space 这个玩意,就是存储对象的映射关系的,其实就是隐藏类 (opens new window)
- code space 简单点说,就是存放代码的地方,编译之后的代码,最大限制为 512MB,也是唯一拥有执行权限的内存
# 标记和清除/整理
老生代垃圾回收器会先使用标记 - 清除(Mark-Sweep)算法进行垃圾回收。
# 第一步,进行标记-清除。
这个过程在《JavaScript 高级程序设计(第三版)》中有过详细的介绍,主要分成两个阶段,即标记阶段和清除阶段。首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的清除阶段对其进行空间的回收。
当然这又会引发内存碎片的问题,存活对象的空间不连续对后续的空间分配造成障碍。老生代又是如何处理这个问题的呢
# 第二步,整理内存碎片
V8 的解决方式非常简单粗暴,在清除阶段结束后,把存活的对象全部往一端靠拢。
# 垃圾回收优化策略(Orinoco)
在以往,新/老生带都包括在内,为了防止逻辑和垃圾回收的情况不一致,需要停止 JS 的运行,专门来遍历去遍历/复制,标记/清除,这个停顿就是:全停顿。
这就比较恶心了,新生代也就算了,本身内存不大,时间上也不明显,但是在老生代中,如果遍历的对象太多,太大,用户在此时,是有可能明显感到页面卡顿的,体验嘎嘎差。
所以在 V8 引擎在名为 Orinoco 项目中,做了三个事情,当然只针对老生代,新生代这个后浪还是可以的,效率贼拉的高,优化空间不大。三个事情分别是:
- 增量标记
将原来一口气去标记的事情,做成分步去做,每次内存占用达到一定的量或者多次进入写屏障的时候,就暂时停止 JS 程序,做一次最多几十毫秒的标记 marking,当下次 GC 的时候,反正前面都标记好了,开始清除就行了
- 并行回收
从字面意思看并行,就是在一次全量垃圾回收的过程中,就是 V8 引擎通过开启若干辅助线程,一起来清除垃圾,可以极大的减少垃圾回收的时间,很优秀,手动点赞
- 并发回收
并发就是在 JS 主线程运行的时候,同时开启辅助线程,清理和主线程没有任何逻辑关系的垃圾,当然,需要写屏障来保障