垃圾回收(Garbage Collection, GC)
浏览器使用的垃圾回收方法:
1. 引用计数(Reference Counting):
- 在这种方法中,每个对象都有一个与之关联的计数器,记录有多少引用指向该对象。当引用计数降至零时,说明没有任何引用指向该对象,因此可以安全地回收。此算法简单但容易造成循环引用问题,导致内存泄漏。
2. 标记-清除(Mark-Sweep):
- 这是最基本也是最早的垃圾回收算法之一。算法分为两个阶段:标记阶段和清除阶段。在标记阶段,GC遍历所有从根开始可达的对象,并标记它们;在清除阶段,未被标记的对象被视为垃圾并回收。这种方法的缺点是它会导致内存碎片化,可能影响性能。
function func(n){ // 标记 n 进入,函数作用域 { var a = 1; // 标记 a 进入,变量提升,函数作用域 let b = 2; // 标记 b 进入,块级作用域 const c = 3; // 标记 c 进入,块级作用域 n = n * a * b; } // 标记 b、c 退出,b、c 等待垃圾回收, // 垃圾回收程序可提前介入,不一定要等函数作用域返回; // 垃圾回收程序可以在任何时刻运行,它的触发时机取决于JavaScript引擎的实现; console.log(n); ... return n;// 标记 n、a 退出, n、a 等待垃圾回收 }
let
、const
对 gc 的帮助:限制作用域,避免变量提升,帮助块级垃圾回收;const
还可降低 因重复或无用赋值引起的内存泄露 风险;
3. 停止-复制(Stop-and-Copy):
-
此方法将内存划分成两块,一块处于使用中,另一块处于空闲状态。垃圾回收时,活动对象从当前使用的内存块复制到空闲块,然后两块的角色互换。这种方法解决了内存碎片问题,但要求有一半的内存空间处于闲置状态,降低了内存使用效率。
-
停止-复制算法的步骤:
flowchart LR B[停止程序执行] --> C[标记存活对象] C --> D[复制存活对象到新内存] D --> E[更新引用到新对象地址] E --> F[清理原内存区域] F --> G[恢复程序执行]
-
停止(Stop):
-
算法"暂停"程序的执行,以便GC可以执行其任务而不受干扰。
-
标记(Mark):
-
GC遍历所有从根集(root set,例如全局变量、活跃的栈帧中的局部变量等)
可达
的对象。标记所有存活的对象,此处的存活指的是程序还可能访问到的对象。
-
复制(Copy):
-
标记为存活的对象被复制到一个新的内存区块。当复制过程完成后,存活对象都将位于这个新的区块中,通常是连续的,这有助于减少内存碎片。
-
在复制过程中,每个被复制的对象的地址都会被更新。确保程序中的所有引用都指向新的对象位置。
-
清理(Cleanup):
-
原来的内存区域(包括所有未被标记为存活的对象)将被视为"垃圾",并且被整个释放,换句话说,这部分内存变得可以重新分配。
-
事实上,经过复制后,原来的内存区域中的大部分或全部内容都将成为无用数据。
-
恢复(Resume):
- 一旦GC完成其任务,程序的执行会从之前停止的地方继续恢复。
停止-复制算法的优缺点:
优点:
- 垃圾内存的清理是彻底的,因为整个旧内存区域都会被释放。
- 减少内存碎片,因为存活的对象在新内存中是连续放置的。
- 实现相对简单。因为清理过程仅仅涉及释放整个旧的存储区。
缺点:
- 需要额外的内存。因为在复制存活对象时,必须有足够的额外空间来存放所有存活的对象。
- 停止整个程序可能会导致性能问题,特别是在实时系统中,长时间的停顿是不可容忍的。
- 复制操作可能引起一定的性能开销,尤其是当存活对象较多时。
-
4. 增量回收(Incremental GC):
- 增量GC通过将垃圾收集的工作分割成小块来执行,从而减少每次垃圾回收引起的停顿时间。这种方法虽然提高了应用的响应性,但由于GC的片断可能导致管理复杂度增加和整体延长垃圾回收时间。
-
增量垃圾收集的步骤:
flowchart LR B[触发增量GC] --> C[执行部分GC任务] C --> D{完成?} D -- 是 --> E[GC结束] D -- 否 --> F[应用继续执行] F --> C E --> G[应用恢复]
- 触发增量GC: 系统根据内存使用情况或设定的阈值触发增量垃圾回收。
- 执行部分GC任务: 执行一小部分的垃圾回收任务,如标记一部分可达对象。
- 检查是否完成: 检查是否所有的垃圾回收任务已完成。
- 结束GC: 如果所有任务完成,则结束垃圾回收。
- 继续应用执行: 如果GC未完成,允许应用程序执行一段时间,可能是执行正常的业务逻辑。
5. 分代回收(Generational GC):
- 基于对象的生命周期不同,将内存分为新生代和老生代。新生代存放生命周期较短的对象,使用高效的算法如Scavenge。老生代存放生命周期长的对象,使用如标记-清除或标记-整理的算法。这种策略的优点是可以针对不同的对象使用最适合的回收技术,优化内存使用和回收效率。
-
分代垃圾回收步骤:
flowchart LR B[对象创建] --> C{对象存活?} C -- 是 --> D[对象转移到老生代] C -- 否 --> E[对象回收] D --> F{GC触发?} F -- Minor GC --> G[新生代GC] F -- Major/Full GC --> H[全面GC-包括老生代] G --> I[继续应用执行] H --> I[继续应用执行]
-
分代垃圾回收通常将对象分为二或三代:
- 新生代(Young Generation):
- 新创建的对象首先放入此区域。
- 因为大量对象很快变得无用,所以经常进行垃圾回收,即“Minor GC”。
- 回收速度快,使用Copying或者Scavenge算法。
- 老生代(Old Generation/Tenured Generation):
- 经过多次回收仍然存活的对象会被晋升到老生代。
- 老生代的垃圾回收频率较低但更耗时,即“Major GC”或“Full GC”,通常采用标记-清除或标记-压缩算法。
- 永久代(Permanent Generation,特定于某些JVM,如Sun/Oracle的HotSpot):
- 存储类元数据和方法信息等。
- 在一些现代的JVM中(如Oracle JDK 8+),永久代已由元空间(Metaspace)取代。
6. 并发和并行垃圾回收:
- 并发GC允许垃圾收集器在应用程序的执行过程中运行
- 并行GC利用多核处理器同时执行垃圾回收任务。
主要的 JavaScript 引擎中的 GC 策略:
1. V8 引擎
-
分代收集:V8使用分代垃圾收集策略,将对象分为新生代和老生代两个部分。
-
新生代收集:
- 新生代:存放生命周期短的小对象。这部分空间较小,但经常进行垃圾回收,即 Minor GC。由于新生代中的对象生命周期短,大多数对象很快就成为垃圾,因此使用高效的算法来快速清理这些对象是合理的
- 新生代内存大小: 64位系统-32MB,32位系统-16M。
- 新生代内存分为两个区域,From space(活跃)、To space(闲置),各占一半。
- 使用 Scavenge 算法,进行半空间复制:
- 对象生成时首先被分配在 From space;
- 当 From space 区域填满时进行垃圾回收,将 From space 中仍然存活的对象复制到 To space,然后清空 From Space;
- 反转两个区域,完成垃圾回收;
-
老生代收集:
- 老生代:存放生命周期长或大对象。老生代的垃圾回收频率较低,使用的是 Major GC,也称为 Full GC。
- 老生代内存大小: 64位系统-1.4GB,32位系统-0.7GB
- 老生代由新生代晋升,晋升条件:对象在新生代存活足够长的时间(经过几次GC还存活);还包括对象的大小和当前新生代空间的使用情况。
- 老生代内存分区:
- 老生代存储区(Old Space):
- 这是存储大多数老对象的主要空间。
- 主要存放那些从新生代中晋升过来的对象。
- 大对象空间(Large Object Space):
- 用于存储大型对象,即那些超过新生代大小限制的对象。
- 这些对象直接在大对象空间分配,避免了在新生代中因为对象大小过大而导致的频繁复制。
- 大对象空间的垃圾回收相对简单,因为每个大对象都是单独管理,容易确定是否还被引用。
- 代码空间(Code Space):
- 专门用来存放可执行代码。
- 这个空间的对象通常都是编译生成的代码块。
- 映射空间(Map Space):
- 存储结构类型信息(Maps),这些信息定义了对象的结构。
- Maps 是 V8 中所有对象类型定义的基础。
- Cell space, PropertyCell space 和其他:
- 这些较小的空间用于存放特定类型的数据,如闭包的变量绑定等。
- 老生代存储区(Old Space):
- 通过标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法清理老生代
- 分类标记:采用深度优先遍历,遍历每个对象。
- 标记-清除:标记所有从根节点
可达
的对象,清除未标记的对象。 - 标记-整理:在清除未标记对象后,将存活的对象整理移到内存的一端,从而减少内存碎片,提高内存使用效率。
-
增量垃圾回收和延迟清理:为了防止垃圾收集在执行时造成大的延迟,V8 引入了增量标记和延迟清理等技术。
2. SpiderMonkey 引擎
-
精确垃圾收集:SpiderMonkey 使用确切的根跟踪技术,其中编译器在运行时生成GC的根信息。
-
分代收集:类似于 V8,SpiderMonkey 也使用分代收集,包括对新生代的基于副本的回收以及对老生代的标记和清除或标记-整理。
-
增量和并行GC:它能够跨多个GC增量步骤分割垃圾收集的工作,减少每次垃圾回收对程序运行的中断。
3. JavaScriptCore (safari)
-
分代收集:JavaScriptCore 也实现了类似的分代收集框架。
-
并发GC:从某些版本开始,JavaScriptCore 开始采用并发GC,允许垃圾回收与JavaScript代码同时运行,减少程序的暂停时间。