JS垃圾回收机制有哪些?

简介: 本文介绍了JavaScript中的垃圾回收(GC)机制,包括其概念、产生原因及重要性。文章详细讲解了几种常见的垃圾回收算法,如引用计数、标记清除、标记整理和分代回收,并分析了它们的优缺点。最后总结了垃圾回收对JS开发的重要作用,强调了其在自动内存管理和性能优化中的关键地位。

本文首发微信公众号:前端徐徐。

概念

GC

在JavaScript中,GC代表"垃圾回收"(Garbage Collection)。垃圾回收是一种自动内存管理机制,负责监测不再使用的对象,并释放它们占用的内存空间,以避免内存泄漏和资源浪费。

JavaScript是一种高级编程语言,它在运行时动态地创建和销毁对象。当创建对象时,JavaScript引擎会为其分配内存空间,而当对象不再被引用或使用时,内存空间应该被释放以便在后续的代码执行中重新利用。垃圾回收机制就是负责自动检测和清除不再需要的对象,以释放占用的内存。

用通俗的例子来解释的话就是:

假设内存空间是一间房间,里面满满当当都堆着各种各样的东西。这时候你想在房间里面再放新的东西,就要先把一些不要的旧东西清理出去。

JS引擎就像个管家,它会定期查看房间里哪些东西不再被使用或者无法访问到了。这些被遗忘的旧物品就像是内存中的“垃圾”,会被GC收集起来扔出房间。扔出这些垃圾后,房间就空出空间来了,可以继续用来存放新的变量、对象等程序的数据了。

GC就好比是程序的内存管家,不断地检查内存使用情况,发现那些可以被清理的垃圾,然后自动回收这些内存。这对保证JS程序平滑运行非常重要。

垃圾产生原因

  1. 内存分配时产生:JS程序在运行过程中,会不断地申请内存来存储对象、变量等数据。这些内存使用完后就成了“垃圾”。
  2. 不再被引用:由于JS是动态语言,一个对象是否还被使用,只能通过是否有引用来判断。如果对象没有被任何东西引用,它就成为了垃圾。
  3. 全局变量、循环引用:全局变量、闭包、循环引用等情况会导致某些内存无法被释放,这部分内存也会变成垃圾。
  4. 程序运行产生的中间状态:像函数的局部变量等temporal 数据也会占用内存,使用完就成为垃圾

为什么要回收垃圾

  1. 管理内存使用:JS中内存的分配和释放都是自动的,为了控制内存占用,需要有垃圾回收机制来回收未使用的内存。
  2. 防止内存泄漏:由于对象之间相互引用,可能导致一些未被使用的对象无法释放,垃圾回收可以找到并释放这些对象。
  3. 释放内存:定期回收不再需要的对象内存,释放内存空间让程序重新使用。
  4. 提高性能:释放未使用的内存可以使得内存使用更高效,减少内存碎片,提高程序性能。
  5. 优化内存:垃圾回收可以按照使用情况优化内存的分配,减少内存占用。
  6. 简化开发:开发者不需要关注内存控制,垃圾回收自动完成内存管理的工作。
  7. 避免常见错误:自动内存管理可以防止手动内存控制常见的错误,例如内存泄漏、访问未分配内存等

几种机制

引用计数

引用计数(Reference Counting)是JS中较早的垃圾回收方式之一。

✴️ 基本思想:

追踪记录每个对象被引用的次数,当一个对象的引用数达到 0 的时候,说明这个对象不再被使用,那么就可以将其占用的内存空间回收释放。每个对象有一个引用计数属性,初始值为0 ==> 当对象被引用时,其计数器加1 ==> 当引用失效时,计数器减1 ==> 当计数器为0时,表示对象不再需要,该对象的内存空间可以被回收。

优点:

  1. 实现简单,资源消耗较少
  2. 可以立即回收内存,不需要等到执行周期结束
  3. 最大限度地减少程序暂停时间

缺点:

  1. 循环引用问题:如果两个或多个对象相互引用,它们的引用计数永远不会变为0,即使它们已经不再被程序访问,也不会被回收,从而导致内存泄漏。
  2. 计数器更新开销:在引用计数中,每当对象被引用或取消引用时,都需要更新计数器。这个开销会影响性能,特别是在存在大量对象的情况下。
  3. 不可达对象问题:引用计数无法处理循环引用之外的其他类型的不可达对象。例如,如果有一个对象A引用了B,而B又引用了C,但C不再被任何其他对象引用,那么C将无法被回收。

由于以上问题,现代的JavaScript引擎通常不再采用纯粹的引用计数作为主要的垃圾回收机制。

标记清除

标记清除(Mark and Sweep)是JavaScript中最常见的垃圾回收机制之一。它是一种用于自动内存管理的算法,用于检测和释放不再使用的对象,以避免内存泄漏和内存溢出。

✴️ 基本思想:

  1. 标记阶段:垃圾回收器从根对象(通常是全局对象、活动执行栈和闭包等)出发,遍历所有可访问的对象,并标记为活动对象。在这个阶段,垃圾回收器会识别出所有被引用的对象,将其标记为“存活”。
  2. 清除阶段:在标记阶段完成后,垃圾回收器会对堆内存进行扫描,清除所有未标记的对象,这些对象被认为是“垃圾”,因为它们不再被任何活动对象引用。清除阶段会释放这些垃圾对象所占用的内存空间,使其可用于未来的对象分配。

标记清除算法通过这两个阶段实现垃圾对象的回收。在标记阶段,垃圾回收器会确定哪些对象仍然处于活动状态。在清除阶段,垃圾回收器会清理所有未被标记的对象,释放内存空间。

优点:

  1. 标记和清除两个阶段执行效率较高
  2. 不需要额外的内存开销
  3. 可以立即回收可回收对象的内存

缺点:

  1. 会产生内存碎片,降低内存利用率
  2. 需要暂停程序执行进行完整垃圾回收,可能造成较长的停顿时间
  3. 清除时需要遍历所有对象,比较低效
  4. 标记和清除效率依赖于对象数量,对象越多越慢
  5. 不易实现增量式垃圾回收

标记整理

标记整理(Mark-and-Compact)是在基本标记清除算法基础上的改进。

✴️ 基本思想:

  1. 标记阶段:同标记清除一样,首先标记所有正在被引用的对象。
  2. 整理阶段:与直接清除不同,标记整理会先执行一次内存整理。将存活对象向内存空间一端移动,然后直接清理掉端边界以外的内存。
  3. 清除阶段:空间端边界以外的内存就全部可以直接回收掉了。

优点:

  1. 减少内存碎片,内存利用率高
  2. 不需要按顺序回收,回收速度快
  3. 新的对象分配速度也较快

缺点:存活对象移动需要额外时间与计算资源。标记整理对标记清除进行了改进和优化,应用更加广泛。

分代回收

分代回收(Generational Garbage Collection)是 JS 垃圾回收机制中的重要优化,是现代JavaScript引擎中采用的一种高效垃圾回收策略,它可以显著提高应用的性能和内存管理效率

✴️ 基本思想:

将内存中的对象分为新生代和老生代,根据对象的生命周期不同进行不同策略的回收。在分代回收的策略中,新创建的对象首先分配到新生代。垃圾回收器在新生代执行频繁的小规模垃圾回收,通常采用快速而简单的算法(比如Scavenge算法)来清理不再使用的对象。当对象在新生代经历了一定次数的垃圾回收仍然存活时,它们将被晋升到老生代。老生代的垃圾回收较为复杂,可能会涉及更多的算法和更大的回收范围。

优点:

通过区分不同对象的生命周期,它可以更精确地选择垃圾回收的时机和策略。大多数对象在创建后很快就变得不可访问,因此将它们分配到新生代,并频繁进行小规模的垃圾回收,可以有效地释放短期存活对象的内存。而那些长期存活的对象,由于它们在老生代,可能需要较长时间才会进行垃圾回收,从而避免频繁地进行大规模回收,提高了性能。

缺点:

  1. 实现复杂度高:分代回收需要维护对象年龄和代的信息,不同代采用不同算法,实现比较复杂。
  2. 内存开销大:记录额外的代信息需要占用内存资源。
  3. 参数依赖性强:分代大小、对象晋升年龄等参数的设置对效果有很大影响。
  4. 问题未完全解决:分代回收只是缓解了频繁回收问题,对象生命周期不固定,仍需全堆回收。
  5. 会产生内存碎片:分代独立会导致新生代频繁回收产生大量碎片。

空闲时垃圾回收

空闲时垃圾回收(Idle-time Garbage Collection)是JS垃圾回收机制的一种常见优化技术,用于在系统空闲或闲置时进行垃圾回收,以减少对应用性能的影响。这种技术旨在在用户不活跃或浏览器空闲时,利用系统资源进行垃圾回收,而不会对正在运行的应用产生明显的影响。

✴️ 基本思想:

  1. JS引擎监测代码执行情况,判断程序何时进入空闲状态。
  2. 在代码空闲执行期间启动垃圾回收,利用CPU空闲资源。
  3. 代码执行需要CPU时暂停垃圾回收,切换资源服务代码运行。
  4. 交替进行回收和执行,将回收工作分散到不同空隙中。

优点:

  1. 减少垃圾回收过程对代码执行流程的影响和干扰。
  2. 避免垃圾回收时产生长时间的执行停顿。
  3. 提高用户体验,减少垃圾回收造成的卡顿感。

缺点:空闲时垃圾回收并不意味着永远不会有任何垃圾回收暂停。在某些情况下,垃圾回收器可能仍需要在运行时进行一些必要的回收操作。空闲时垃圾回收只是在合适的时机尽量减少对应用的影响。

总结

垃圾回收是JS自动内存管理非常重要的一部分,可以自动回收不再需要的内存对象,防止内存泄漏。主要的垃圾回收算法包括标记清除,标记整理,引用计数等。现代浏览器一般combine多个算法来实现。为了优化效率,会使用分代回收适当区分新生和老生对象,以及闲时回收利用CPU空闲资源。垃圾回收会有一定的性能影响,我们应该编写高质量代码来配合,减少不必要的内存占用。

总的来说,垃圾回收机制使得JS开发人员不再需要关注内存控制这块复杂的工作。它极大地简化了JS的内存管理,使开发人员可以专注于业务代码的实现。这是JS作为一门优秀动态语言的重要支撑。

相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1529 4
|
18小时前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
6天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
524 20
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
186 2
|
9天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
486 5
|
8天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
318 2
|
5天前
|
XML 安全 Java
【Maven】依赖管理,Maven仓库,Maven核心功能
【Maven】依赖管理,Maven仓库,Maven核心功能
200 2