深入理解 JS —— 垃圾回收
垃圾回收(Garbage Collection,简称GC)是指自动回收不再使用的内存,从而避免内存泄漏和提高程序性能的过程。当一个对象不再被引用时,GC 会自动释放它占用的内存空间。这样,我们就不需要手动管理内存分配和释放,从而降低了程序出错的概率。
垃圾回收的算法
JavaScript 垃圾回收机制主要使用两种算法:标记-清除(Mark-and-Sweep)和引用计数(Reference Counting)。
引用计数(Reference Counting)
引用计数的基本原理是,跟踪每个对象的引用数量。当一个对象被引用时,它的引用计数就增加;当某个引用不再指向该对象时,引用计数就减少。当一个对象的引用计数为零时,说明该对象已经不再被任何地方引用,此时可以将其销毁,释放内存。
优点:实现简单,实时性强(引用值为 0 时立刻回收),内存管理高效。
缺点:存在循环引用的问题。例如,两个对象互相引用,它们的引用计数永远不为零,导致无法释放内存。
标记-清除(Mark-and-Sweep)
标记-清除是目前 JavaScript 引擎中使用最广泛的垃圾回收算法。其步骤如下:
- 标记阶段:从根对象(如全局对象、局部变量、活动函数等)开始,递归地访问所有可达的对象,标记它们为“活动”。
- 清除阶段:遍历内存中的所有对象,如果一个对象没有被标记为“活动”,那么它就是垃圾对象,GC 会将其销毁,并释放内存。
优点:不容易受到循环引用问题的影响。(通过标记整理算法还可以解决空闲内存不连续的问题)
缺点:标记-清除需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC
,还需要遍历堆里的活动以及非活动对象来清除,可能导致程序的性能下降。
V8对GC的优化
分代式垃圾回收(Generational)
分代式垃圾回收是一种优化垃圾回收过程的策略,其核心思想是根据对象的生命周期将内存分为多个“代”(generation)。这种方法假设大多数对象在被创建后会很快变得不再需要,而只有少数对象会长时间存活。因此,垃圾回收机制根据对象的年龄将它们分配到不同的代,并对不同代的对象使用不同的垃圾回收策略。
新老生代
在 V8 引擎中,堆内存被分为新生代和老生代两个区域,以便针对不同类型的对象采用不同的垃圾回收策略。
- 新生代存放生命周期较短的对象,通常是刚创建的对象,内存容量相对较小(一般为 1~8MB)。
- 老生代则存放生命周期较长或常驻内存的对象,通常是经过多次垃圾回收仍然存活下来的对象,内存容量通常较大。
新生代垃圾回收:Cheney 算法
在 V8 引擎中,新生代的垃圾回收是通过一种名为 Scavenge 的算法来实现的,Scavenge 算法本质上采用了 Cheney 算法,这是一种复制式的垃圾回收方法。通过这种方法,堆内存被分为两个区域:使用区 和 空闲区,以便在垃圾回收时更加高效地管理内存。
堆内存的划分
堆内存在垃圾回收之前会被分为两部分:
使用区:存放活动对象,也就是正在使用的内存区域。
空闲区:空闲的内存区域,用来存放经过回收后的活动对象。
所有新创建的对象都会被放入使用区。当使用区即将填满时,垃圾回收机制便会触发,开始清理无效的对象。
垃圾回收过程
垃圾回收的过程会按照以下步骤进行:
- 标记活动对象:首先,垃圾回收器会扫描使用区中的对象,并标记所有活动对象(即仍然被引用的对象)。
- 复制到空闲区:接着,标记的活动对象会被复制到空闲区,并按顺序整理,确保空闲区内的对象是有效且紧凑的。
- 清理无效对象:一旦活动对象被复制,空闲区会清理掉不再使用的对象,释放内存。
- 角色互换:完成复制和清理后,使用区和空闲区会互换角色。原先的使用区变成空闲区,而原先的空闲区则成为新的使用区,准备好接收新对象。
这种复制的过程能有效地整理内存,减少碎片化,并确保每次垃圾回收都能有一个清晰的内存结构。
对象晋升到老生代
如果一个对象在经过多次复制后仍然存活,说明它的生命周期较长,可能会成为老生代的一部分。为了避免新生代的回收过于频繁,存活较久的对象会被晋升到老生代,并采用更为复杂的垃圾回收策略进行管理。
内存占用过高的处理
此外,Cheney 算法还设定了一个阈值:当空闲区的空间占用超过 25% 时,任何复制到空闲区的对象会直接晋升到老生代。这是因为如果空闲区的空间占比过高,可能会影响到后续内存分配,导致垃圾回收变得低效。因此,这个 25% 的限制确保了内存分配过程的高效性。
老生代垃圾回收:标记清除与标记整理算法
相比新生代,老生代的垃圾回收过程就显得相对简单一些。通常,生命周期较长、占用内存较大的对象会被分配到老生代。由于老生代中的对象通常比较大,如果也像新生代那样采用复制的方式进行垃圾回收,会显得非常低效。因此,V8 在老生代的垃圾回收过程中采用了 标记清除 和 标记整理 算法,以提高回收效率并避免内存碎片问题。
为什么需要分代式?
分代式垃圾回收的核心目的是优化内存管理,基于对象的生命周期差异,提高垃圾回收效率。大多数对象在创建后很快就不再需要,因此将它们放入新生代进行高频次、高效率的回收;而生命周期较长的对象则被晋升到老生代,避免频繁的回收操作。通过这种分代管理,分代式垃圾回收能有效减少内存碎片,提高内存利用率,并减少垃圾回收对性能的影响,尤其是在处理大量短期对象和少量长期对象的场景中表现尤为突出。
并行垃圾回收(Parallel)
并行垃圾回收是一种通过多线程并行执行垃圾回收过程来提高回收效率的策略。在现代 JavaScript 引擎中,垃圾回收过程通常会涉及多个阶段(如标记、清理等),这些操作会消耗大量的 CPU 资源,尤其是在处理老生代时,垃圾回收的复杂度较高,可能导致应用的长时间停顿。为了减少这种停顿并提升回收效率,V8 等引擎采用了并行垃圾回收。
基本概念
并行垃圾回收通过在多个 CPU 核心上同时执行回收任务来加速垃圾回收过程。垃圾回收的各个阶段,特别是老生代的回收,通常需要遍历大量对象,这些操作会消耗大量的 CPU 时间。通过并行化处理,可以将这些操作分配到多个线程上并行执行,从而减少垃圾回收的总体时间。
优势
- 减少停顿时间:通过并行处理,多个线程可以同时执行垃圾回收任务,从而缩短了每次垃圾回收的执行时间,避免了长时间的停顿,提升了应用的响应性。
- 提升性能:在多核 CPU 上,利用并行垃圾回收可以显著提升回收效率,避免单个线程占用过多 CPU 时间,提升整体性能。
- 更高效的内存管理:尤其是在老生代的垃圾回收中,较长生命周期的对象需要进行更复杂的回收操作,通过并行化可以更快速地处理这些复杂任务,避免性能瓶颈。
增量垃圾回收(Incremental)
增量垃圾回收是一种将垃圾回收任务分割成多个小块,逐步完成的策略,旨在减少垃圾回收的停顿时间,避免长时间的应用卡顿。与传统的全量垃圾回收相比,增量垃圾回收通过分阶段执行垃圾回收任务,让应用能够持续运行,减少回收过程对用户体验的影响,特别是在需要频繁回收的场景中,如处理大量短期对象的新生代。
基本概念
增量垃圾回收的主要思想是将垃圾回收的工作分解成多个小步骤,而不是一次性执行完所有的标记和清理工作。每个小步骤执行一部分任务,执行时间短,应用可以在这些步骤之间间歇性地恢复到正常操作。这意味着垃圾回收不再需要一次性地暂停应用,而是将回收任务分布到多个周期中去,减少了垃圾回收带来的应用停顿时间。
优势
- 减少停顿时间:通过将垃圾回收任务分散到多个阶段,增量垃圾回收能够避免一次性的大规模停顿。每次回收停顿的时间都被限制在一个可接受的范围内,使得用户不会察觉到明显的卡顿。
- 提高响应性:增量回收确保了应用在垃圾回收的过程中可以保持较高的响应性,因为它允许回收任务在空闲时间段执行,而不是让应用长时间停顿。
- 适应性强:增量垃圾回收可以更好地适应动态内存分配和回收的需求,尤其适用于新生代等频繁回收的内存区域,能够有效减少频繁回收对应用的影响。