JS垃圾回收机制
在 JavaScript 中,内存管理是开发者经常忽略却至关重要的一部分。与某些底层语言不同,JavaScript 的运行环境(如浏览器和 Node.js)配备了自动垃圾回收机制,帮助开发者管理内存分配和释放,避免手动清理的复杂性。然而,这种“自动化”并不意味着完全无需关注。了解垃圾回收的原理、运行机制和潜在问题,不仅能帮助我们优化代码性能,还能有效避免内存泄漏等隐性问题。本文将从 JavaScript 垃圾回收机制的核心概念入手,解析其工作原理及在实际开发中的重要性
引用计数法
引用计数法是早期垃圾回收算法的一种,它通过追踪每个对象的引用次数来判断该对象是否可以被回收。在 JavaScript 的垃圾回收机制中,引用计数法是重要的一部分,尽管它已不再是主流,但理解这一机制对于掌握 JavaScript 内存管理至关重要
引用计数法的核心思想是:
- 每个对象维护一个 引用计数器,表示该对象被引用的次数
- 增加引用:当一个变量引用该对象时,引用计数 +1
- 减少引用:当一个引用失效(如变量被重新赋值为 null 或指向其他对象)时,引用计数 -1
- 回收对象:当某个对象的引用计数为 0 时,认为该对象不可达,即无其他部分会使用它,垃圾回收器会释放该对象占用的内存
这种垃圾回收机制虽然简单,但是存在明显的缺陷:
- 优点:引用计数只需要在计数为0 时就可以回收,不需要遍历所有对象
- 缺点:计数器需要占很大的空间,无法解决循环引用无法回收的问题
标记清除法
标记清除法(Mark-and-Sweep)是现代 JavaScript 引擎中最常用的一种垃圾回收算法。它有效解决了引用计数法的循环引用问题,并成为垃圾回收机制的核心基础算法。标记清除法通过标记可达对象和清除不可达对象的方式,自动释放内存中不再使用的数据
工作流程:
- 根对象出发:从全局对象(如 window 或 global)以及当前执行上下文的作用域链开始,找到所有可达对象
- 标记阶段:标记所有可达对象,表示它们是活跃的
- 清除阶段:扫描内存中未被标记的对象,将其占用的内存释放
- 内存整理(可选):有些引擎会对内存中的对象进行整理,减少碎片化,提高内存分配效率
标记清除法的局限性:
- 暂停执行的性能开销:标记清除法需要扫描内存中的所有对象,标记和清除过程会暂停应用程序的执行(即 Stop-the-World),在大规模对象中可能导致卡顿
- 内存碎片化问题:清除不可达对象后,内存中可能会留下很多不连续的小空闲空间,导致内存碎片化,影响后续的内存分配
- 频繁触发影响性能:如果垃圾回收器频繁运行(如在高密集内存使用场景中),可能会对应用性能产生明显影响
分代式回收
为了解决标记清除法的局限性,现代 JavaScript 引擎对其进行了多种优化,如 V8 引擎广泛采用了分代式垃圾回收机制
对象的生命周期长短具有规律性,大多数对象的生命周期较短,少数对象的生命周期较长。 基于此,分代式垃圾回收将内存分为两部分:
- 新生代(Young Generation):存储生命周期短的对象
- 老生代(Old Generation):存储生命周期长的对象
新生代
新生代使用Scavenge算法、Cheney算法,Cheney算法将新生代内存一分为二,使用区和空闲区,对活动区的对象进行标记,清理,活动区和空闲区身份互换,交换两次后,将活动区还在的老对象放到老生代,如果交换后空闲区超过25%,将会把对象都放到老生代
老生代
老生代存放一些空间大、存活时间长,如果放在新生代频繁复制交换,会非常耗时;老生代使用标记清除法,进行清除,排序整理,标记整理并行回收,读写锁全停顿,增量标记、三色标记、写屏障惰性清理
并行回收
并行回收是一种优化的垃圾回收技术,它通过利用多核 CPU 的优势,在垃圾回收阶段同时运行多个线程,以加速垃圾回收过程。在传统垃圾回收机制中,垃圾回收器通常是单线程运行的,所有的回收任务都由一个线程顺序执行。当垃圾回收过程启动时,整个程序会暂停(即“Stop-the-World”)。随着现代计算机硬件的多核化,利用多线程并行处理垃圾回收任务,能显著缩短回收时间
工作流程:
(1)
标记阶段的并行化
:垃圾回收器会从根对象(如全局对象、函数调用栈)开始,递归标记所有可达的对象。 在并行回收中,多个线程同时参与标记任务,分工处理不同的内存区域,减少单线程标记的耗时(2)
清除阶段的并行化
:对未标记的不可达对象进行清理,并释放其占用的内存 并行回收通过多线程同时扫描内存的不同区域,加速垃圾的清除过程。
增量标记
增量标记将垃圾回收中的标记阶段分解为多个小的增量步骤,允许垃圾回收器和应用程序线程交替运行。这种方式特别适合对实时性要求较高的应用程序场景,如交互式界面或游戏开发
增量标记的核心在于将标记阶段分解为多个小步骤,而不是一次性完成整个标记过程。通过这种方式,应用程序线程在垃圾回收过程中能够继续执行,从而减少用户感知到的卡顿现象
在增量标记中,垃圾回收器使用了写屏障(Write Barrier)和三色标记法来保证标记过程的一致性
三色标记法
增量标记中,对象分为三种颜色,以追踪其标记状态:
- 白色(未访问):尚未被标记的对象,默认所有对象最初为白色
- 灰色(已访问但未处理引用):标记为可达,但其引用的对象尚未全部标记
- 黑色(已访问且已处理引用):标记为可达,且其引用的对象已全部标记
垃圾回收器在增量阶段逐步将对象从白色转为灰色,再转为黑色
写屏障
写屏障是一种机制,用于跟踪增量标记阶段对象引用的变动
如果应用程序线程修改了对象的引用关系,写屏障会记录下变动的对象,并在下一次标记时处理这些对象; 这样可以保证增量标记过程的正确性,避免漏标对象
惰性清除
传统的垃圾回收算法(如标记-清除)中,在标记阶段标记出不可达对象后,清除阶段会一次性清理所有垃圾对象。这种方式虽然简单,但当内存中的垃圾对象较多时,清除阶段可能会导致应用程序暂停较长时间,从而影响用户体验
惰性清除的核心在于延迟清除垃圾对象,并将清除任务分散到后续的内存分配或空闲时间中。具体表现为:
- 标记阶段:像传统的标记-清除算法一样,标记所有可达对象
- 清除阶段:不立即清除所有未标记的对象,而是将它们保留在空闲列表中;在下一次分配内存时,只清除需要的部分垃圾对象以腾出足够的空间
并发回收
V8 引擎采用了 分代回收(Generational Garbage Collection)和 并发标记清除(Concurrent Mark-and-Sweep)算法
- 分代回收:V8 会将对象分为 年轻代(Young Generation)和 老年代(Old Generation)。年轻代的对象生命周期较短,回收频繁;老年代的对象生命周期较长,回收较少
- 并发标记清除:垃圾回收的标记阶段在后台线程中与应用程序代码并发执行。清除阶段可能会暂停应用程序,但可以使用 增量清除 和 并发清除 的策略,减少停顿时间
并发回收的优势:
- 减少停顿时间:传统的垃圾回收可能会导致长时间的停顿,影响用户体验。并发回收可以将垃圾回收工作分散到多个时间点进行,从而缩短每次停顿的时间
- 平滑用户体验:通过并发执行,应用程序的响应性得到了保持,避免了长时间卡顿