大家好,今天分享的主题为 JavaScript 内存管理机制,本次分享将从以下三部分进行讲述:
- js 内存管理与 js 垃圾
- 常见的 GC 算法
- V8 引擎的垃圾回收
js 内存管理与 js 垃圾
关于 JavaScript 内存管理机制,相信大家都有所了解。我们就简单看一下 js 内存管理与 js 垃圾。JavaScript 内存管理是由 JS 自动操作的,不需要人为进行参与,这些内存管理包含以下三项:
- 申请内存空间
- 使用内容空间
- 释放内容空间
而 js 垃圾是指对象不在引用时、对象不能从根上访问到时,都可以被称为 js 垃圾。其他部分包括引用和可达对象这些大家肯定很熟悉了,我们就不再多说。下面我们谈一谈 GC 算法。
GC 算法
GC 算法其实是为了找到内存中的垃圾,并释放和回收空间。这里所说的的垃圾,是指算法中认为程序中不再需要使用的对象,与程序中不能访问到的对象。
说回 GC 算法,这个是比较概念性的内容,我们简单归纳一下。GC 是一种自内存中查找垃圾释放空间、回收空间的一个垃圾回收器机制。算法则是工作时查找和回收所遵循的规则。常见 GC 算法有引用计数、标记清除、标记整理、分代回收。
引用计数
引用计数曾经主要用于 IE8 以下的浏览器,现在的浏览器已不再使用,因此只做简单介绍。引用计数的基本原理是记录跟踪每一个值被引用的次数,被引用则计数加一,被释放则减一,当数值为零时则代表该值所在内存已经不再使用,因此释放所占空间。引用计数的优点是引用次数实时监控,所以回收垃圾能够及时回收,从而最大限度减少程序暂停卡顿时间。但也是因为一直在运作,所以资源消耗和时间开销大,无法回收循环引用的对象。
标记清除
标记清除分为分为标记和清除两个阶段,其核心思想是遍历所有对象,找标记活动对象,即前面提到的可达对象,清除没有标记的对象,以及回收没有标记对象的空间。
上图是 global 的查找简易流程图。其中左侧 A、B、C、D、E 表示可查到的对象,右侧 a1、b1 表示循环引用对象。其中 a1 为引用计数,而因为引用计数一直在运作,无法回收循环引用的对象的缺点,可以反向找到正在循环引用的对象。
这也是标记清除的优点,可以解决对象循环的引用回收问题。但是标记清除的缺点是空间碎片化,无法及时回收垃圾对象。因为它需要先标记再清除,不能像引用计数一样对值进行实时监控,因此无法让空间最大化使用。通过下图可以简单看一下标记清除的空间碎片化特点。
标记整理
上面提到标记清除有空间碎片化的缺点,而标记整理优化了这个缺点。从名字也可以联想到,标记整理是标记清除的增强。标记整理在标记阶段的操作和标记清除一致,但是在清除阶段会先执行整理,再进行清除。这种方式能够有效减少碎片化空间。和标记清除一样,标记整理也不能实时回收垃圾对象。
我们通过下面三张图对标记整理进行一个简单直观的了解。
可以看到在进行垃圾回收前,活动空间和非活动空间是混杂的。而在确定进行回收后,标记整理会对空间进行归类整理,将活动空间和非活动空间统一整理到一起,形成下图的结果:
之后再进行标记清除就能够避免回收操作避免出现大量碎片化空间,让空间最大化应用。
看完了 GC 算法,以 V8 引擎为例我们具体来看一下 GC 算法在 JS 垃圾回收里的使用。
V8 引擎的垃圾回收
V8 是一款当下较为主流 JavaScript 执行引擎,采用即时编译,处理速度很快。V8 的内存是设限的,比如 64 位操作系统的上限是 1.4T,下限是 700M,32 位操作系统的上下限分别为 64M 和 32M。
V8 采用分代回收的垃圾回收策略,将内存分为新生代和旧生代两种,并对不同的对象采用不同的对应算法。
上图是 V8 的内存分配示意图,可以清除看到 V8 内存空间分为两部分。左边的 from 和 to 是新生代,占用的空间比较小(32M|16M),这里的新生代指的是存活时间短的存储区。右边红色的部分则是存活时间较长的老生代存储区。
V8 常用的 GC 算法有以下 5 种:
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
这其中新生代采用复制算法和标记整理进行垃圾回收,老生代使用标记清除、标记整理和增量标记进行垃圾回收。
V8 新生代对象回收实现
上图为 V8 新生代对象回收实现图,采用复制算法和标记整理结合的方式进行垃圾回收。新生代内存区的两个等大空间,From 代表使用空间用于存储活动对象,To 代表空闲空间。V8 的新生代对象回收是通过标记整理将对象完成整理后拷贝到 To,然后将 To 和 From 进行空间交换,并释放整理后的无用对象所占空间。需要注意的是,在将整理对象拷贝到 To 时可能会出现晋升。晋升指的是将新生代对象移动至老生代存储区。晋升通常有两个条件,其一是在进行一轮 GC 后还活着的新生代对象可以晋升,其二是 To 空间的使用率超过 25%。
V8 老生代对象回收实现
V8 老生代的回收过程采用标记清除、标记整理和标记增量结合的方式。一般在进行垃圾回收时会通过标记清除完成垃圾空间的回收,但是当新生代移动到老生代,而老生代内存不够时,则会通过标记整理进行空间优化,并使用增量标记进行效率优化。
标记增量其实是通过对标记操作进行标记的方法,让时间安排变得合理。这句话可能有些绕,简单说就是在垃圾回收时,让标记系统在标记时分出不同的时间段,分别进行标记和执行,让二者的操作间隔开,从而优化时间安排,这会让页面在体感上更为顺畅。