JS垃圾回收机制
概念
GC
在 JavaScript 中,GC 代表"垃圾回收"(Garbage Collection)。垃圾回收是一种自动内存管理机制,负责监测不再使用的对象,并释放它们占用的内存空间,以避免内存泄漏和资源浪费。
JavaScript 是一种高级编程语言,它在运行时动态地创建和销毁对象。当创建对象时,JavaScript 引擎会为其分配内存空间,而当对象不再被引用或使用时,内存空间应该被释放以便在后续的代码执行中重新利用。垃圾回收机制就是负责自动检测和清除不再需要的对象,以释放占用的内存。
用通俗的例子来解释的话就是:
假设内存空间是一间房间,里面满满当当都堆着各种各样的东西。这时候你想在房间里面再放新的东西,就要先把一些不要的旧东西清理出去。
JS 引擎就像个管家,它会定期查看房间里哪些东西不再被使用或者无法访问到了。这些被遗忘的旧物品就像是内存中的“垃圾”,会被 GC 收集起来扔出房间。扔出这些垃圾后,房间就空出空间来了,可以继续用来存放新的变量、对象等程序的数据了。
GC 就好比是程序的内存管家,不断地检查内存使用情况,发现那些可以被清理的垃圾,然后自动回收这些内存。这对保证 JS 程序平滑运行非常重要。
垃圾产生原因
- 内存分配时产生: JS 程序在运行过程中,会不断地申请内存来存储对象、变量等数据。这些内存使用完后就成了“垃圾”。
- 不再被引用: 由于 JS 是动态语言,一个对象是否还被使用,只能通过是否有引用来判断。如果对象没有被任何东西引用,它就成为了垃圾。
- 全局变量、循环引用: 全局变量、闭包、循环引用等情况会导致某些内存无法被释放,这部分内存也会变成垃圾。
- 程序运行产生的中间状态: 像函数的局部变量等 temporal 数据也会占用内存,使用完就成为垃圾
为什么要回收垃圾
- 管理内存使用: JS 中内存的分配和释放都是自动的,为了控制内存占用,需要有垃圾回收机制来回收未使用的内存。
- 防止内存泄漏: 由于对象之间相互引用,可能导致一些未被使用的对象无法释放,垃圾回收可以找到并释放这些对象。
- 释放内存: 定期回收不再需要的对象内存,释放内存空间让程序重新使用。
- 提高性能: 释放未使用的内存可以使得内存使用更高效,减少内存碎片,提高程序性能。
- 优化内存: 垃圾回收可以按照使用情况优化内存的分配,减少内存占用。
- 简化开发: 开发者不需要关注内存控制,垃圾回收自动完成内存管理的工作。
- 避免常见错误: 自动内存管理可以防止手动内存控制常见的错误,例如内存泄漏、访问未分配内存等
几种机制
引用计数
引用计数(Reference Counting)是 JS 中较早的垃圾回收方式之一。
✴️ 基本思想:
追踪记录每个对象被引用的次数,当一个对象的引用数达到 0 的时候,说明这个对象不再被使用,那么就可以将其占用的内存空间回收释放。每个对象有一个引用计数属性,初始值为 0 ==> 当对象被引用时,其计数器加 1 ==> 当引用失效时,计数器减 1 ==> 当计数器为 0 时,表示对象不再需要,该对象的内存空间可以被回收。
✅ 优点:
- 实现简单,资源消耗较少
- 可以立即回收内存,不需要等到执行周期结束
- 最大限度地减少程序暂停时间
❌ 缺点:
- 循环引用问题: 如果两个或多个对象相互引用,它们的引用计数永远不会变为 0,即使它们已经不再被程序访问,也不会被回收,从而导致内存泄漏。
- 计数器更新开销: 在引用计数中,每当对象被引用或取消引用时,都需要更新计数器。这个开销会影响性能,特别是在存在大量对象的情况下。
- 不可达对象问题: 引用计数无法处理循环引用之外的其他类型的不可达对象。例如,如果有一个对象 A 引用了 B,而 B 又引用了 C,但 C 不再被任何其他对象引用,那么 C 将无法被回收。
由于以上问题,现代的 JavaScript 引擎通常不再采用纯粹的引用计数作为主要的垃圾回收机制。
标记清除
标记清除(Mark and Sweep)是 JavaScript 中最常见的垃圾回收机制之一。它是一种用于自动内存管理的算法,用于检测和释放不再使用的对象,以避免内存泄漏和内存溢出。
✴️ 基本思想:
- 标记阶段: 垃圾回收器从根对象(通常是全局对象、活动执行栈和闭包等)出发,遍历所有可访问的对象,并标记为活动对象。在这个阶段,垃圾回收器会识别出所有被引用的对象,将其标记为“存活”。
- 清除阶段: 在标记阶段完成后,垃圾回收器会对堆内存进行扫描,清除所有未标记的对象,这些对象被认为是“垃圾”,因为它们不再被任何活动对象引用。清除阶段会释放这些垃圾对象所占用的内存空间,使其可用于未来的对象分配。
标记清除算法通过这两个阶段实现垃圾对象的回收。在标记阶段,垃圾回收器会确定哪些对象仍然处于活动状态。在清除阶段,垃圾回收器会清理所有未被标记的对象,释放内存空间。
✅ 优点:
- 标记和清除两个阶段执行效率较高
- 不需要额外的内存开销
- 可以立即回收可回收对象的内存
❌ 缺点:
- 会产生内存碎片,降低内存利用率
- 需要暂停程序执行进行完整垃圾回收,可能造成较长的停顿时间
- 清除时需要遍历所有对象,比较低效
- 标记和清除效率依赖于对象数量,对象越多越慢
- 不易实现增量式垃圾回收
标记整理
标记整理(Mark-and-Compact)是在基本标记清除算法基础上的改进。
✴️ 基本思想:
- 标记阶段: 同标记清除一样,首先标记所有正在被引用的对象。
- 整理阶段: 与直接清除不同,标记整理会先执行一次内存整理。将存活对象向内存空间一端移动,然后直接清理掉端边界以外的内存。
- 清除阶段: 空间端边界以外的内存就全部可以直接回收掉了。
✅ 优点:
- 减少内存碎片,内存利用率高
- 不需要按顺序回收,回收速度快
- 新的对象分配速度也较快
❌ 缺点: 存活对象移动需要额外时间与计算资源。标记整理对标记清除进行了改进和优化,应用更加广泛。
分代回收
分代回收(Generational Garbage Collection)是 JS 垃圾回收机制中的重要优化,是现代 JavaScript 引擎中采用的一种高效垃圾回收策略,它可以显著提高应用的性能和内存管理效率
✴️ 基本思想:
将内存中的对象分为新生代和老生代,根据对象的生命周期不同进行不同策略的回收。在分代回收的策略中,新创建的对象首先分配到新生代。垃圾回收器在新生代执行频繁的小规模垃圾回收,通常采用快速而简单的算法(比如 Scavenge 算法)来清理不再使用的对象。当对象在新生代经历了一定次数的垃圾回收仍然存活时,它们将被晋升到老生代。老生代的垃圾回收较为复杂,可能会涉及更多的算法和更大的回收范围。
✅ 优点:
通过区分不同对象的生命周期,它可以更精确地选择垃圾回收的时机和策略。大多数对象在创建后很快就变得不可访问,因此将它们分配到新生代,并频繁进行小规模的垃圾回收,可以有效地释放短期存活对象的内存。而那些长期存活的对象,由于它们在老生代,可能需要较长时间才会进行垃圾回收,从而避免频繁地进行大规模回收,提高了性能。
❌ 缺点:
- 实现复杂度高: 分代回收需要维护对象年龄和代的信息,不同代采用不同算法,实现比较复杂。
- 内存开销大: 记录额外的代信息需要占用内存资源。
- 参数依赖性强: 分代大小、对象晋升年龄等参数的设置对效果有很大影响。
- 问题未完全解决: 分代回收只是缓解了频繁回收问题,对象生命周期不固定,仍需全堆回收。
- 会产生内存碎片: 分代独立会导致新生代频繁回收产生大量碎片。
空闲时垃圾回收
空闲时垃圾回收(Idle-time Garbage Collection)是 JS 垃圾回收机制的一种常见优化技术,用于在系统空闲或闲置时进行垃圾回收,以减少对应用性能的影响。这种技术旨在在用户不活跃或浏览器空闲时,利用系统资源进行垃圾回收,而不会对正在运行的应用产生明显的影响。
✴️ 基本思想:
- JS 引擎监测代码执行情况,判断程序何时进入空闲状态。
- 在代码空闲执行期间启动垃圾回收,利用 CPU 空闲资源。
- 代码执行需要 CPU 时暂停垃圾回收,切换资源服务代码运行。
- 交替进行回收和执行,将回收工作分散到不同空隙中。
✅ 优点:
- 减少垃圾回收过程对代码执行流程的影响和干扰。
- 避免垃圾回收时产生长时间的执行停顿。
- 提高用户体验,减少垃圾回收造成的卡顿感。
❌ 缺点: 空闲时垃圾回收并不意味着永远不会有任何垃圾回收暂停。在某些情况下,垃圾回收器可能仍需要在运行时进行一些必要的回收操作。空闲时垃圾回收只是在合适的时机尽量减少对应用的影响。
总结
垃圾回收是 JS 自动内存管理非常重要的一部分,可以自动回收不再需要的内存对象,防止内存泄漏。主要的垃圾回收算法包括标记清除,标记整理,引用计数等。现代浏览器一般 combine 多个算法来实现。为了优化效率,会使用分代回收适当区分新生和老生对象,以及闲时回收利用 CPU 空闲资源。垃圾回收会有一定的性能影响,我们应该编写高质量代码来配合,减少不必要的内存占用。
总的来说,垃圾回收机制使得 JS 开发人员不再需要关注内存控制这块复杂的工作。它极大地简化了 JS 的内存管理,使开发人员可以专注于业务代码的实现。这是 JS 作为一门优秀动态语言的重要支撑。