请你介绍一下javascript v8是如何进行垃圾回收的?

本文会详细介绍垃圾回收的各个知识点,祝你面试成功。

为什么需要垃圾回收?

核心问题

JavaScript 在运行时需要动态分配内存(如创建对象、数组等),但开发者无法手动释放内存,因此必须依赖自动内存管理机制。

手动 vs 自动

  • 手动管理
    • (如 C/C++):通过 malloc/free 直接控制内存,高效但容易出错(内存泄漏、野指针)
  • 自动管理
    • (如 JavaScript):由引擎自动追踪"不再使用的内存"并回收,安全但有一定性能开销

V8 的内存分代假说

关键洞察

V8 将堆内存分为 新生代(New Space)老生代(Old Space) ,基于两个观察:

  1. 大多数对象"朝生夕死" (如函数内的临时变量)
  2. 存活时间长的对象会持续存活(如全局变量)

内存分区

  • 新生代 :存放新创建的小对象(默认 1~8MB),使用 Scavenge 算法快速回收
  • 老生代 :存放存活时间长的对象(默认约 700MB ~ 1.4GB),使用 标记-清除(Mark-Sweep)标记-整理(Mark-Compact) 算法

新生代垃圾回收(Scavenge)

算法核心 :Cheney 算法(复制算法)
步骤

  1. 将新生代分为两个等大的 From-SpaceTo-Space

  2. 分配对象:新对象始终写入 From-Space

  3. 垃圾回收

    • 遍历 From-Space,将存活对象 复制到 To-Space
    • 交换 From/To 空间角色(清空旧的 From-Space)

优点

  • 时间与 存活对象数量 成正比(适合回收大量"短命"对象)
  • 无内存碎片

对象晋升

  • 若对象在新生代经历多次回收仍存活(默认 2 次)
  • 或 To-Space 使用超过 25%
  • ➔ 晋升到老生代

老生代垃圾回收(Mark-Sweep & Mark-Compact)

标记-清除(Mark-Sweep)

步骤

  1. 标记阶段:从根(全局变量、活动函数栈)出发,递归标记所有可达对象
  2. 清除阶段:遍历堆内存,释放未被标记的对象

缺点

  • 产生内存碎片
  • 全停顿(Stop-The-World)可能较久(影响性能)

标记-整理(Mark-Compact)

改进点 :在清除阶段,将存活对象向一端移动,解决碎片问题
代价:移动对象需要更多时间(通常只在内存碎片较多时触发)

增量标记与并发回收

增量标记(Incremental Marking)

目标 :将标记阶段分解为多个小任务,穿插在主线程的空闲时间执行
实现

  • 使用三色标记法(白→灰→黑)追踪对象状态
  • 通过写屏障(Write Barrier)维护标记一致性

并发标记/清除(Concurrent Marking/Sweeping)

目标 :利用后台线程并行执行标记/清除,减少主线程阻塞
挑战:需保证 JavaScript 执行与回收线程的数据一致性

三色标记法(Tri-color Marking)和写屏障(Write Barrier)是 增量标记(Incremental Marking)并发标记(Concurrent Marking) 的核心技术,用于解决垃圾回收过程中主线程长时间阻塞的问题。下面通过 原理、流程、代码示例 深入解析:

三色标记法

1. 核心思想

将堆内存中的对象标记为三种颜色,表示它们在垃圾回收过程中的状态:

  • 白色:未被访问的对象(默认状态,可能待回收)
  • 灰色:已被访问,但其子引用未被遍历(中间状态)
  • 黑色:已被访问,且所有子引用已被遍历(完成状态)

标记过程从根对象(如全局变量、活动调用栈)出发,最终所有不可达的对象保持白色,被回收。

2. 标记流程

  1. 初始状态

    所有对象标记为白色,根对象变为灰色。

    text 复制代码
    [Root] → A(灰色)  
    B(白色) ← A → C(白色)
  2. 标记阶段

    逐步将灰色对象变为黑色,并遍历其子对象:

    • 取出一个灰色对象(如 A)
    • 遍历 A 的所有子引用(B、C),将子对象标记为灰色
    • A 标记为黑色
    text 复制代码
    A(黑色) → B(灰色)  
    A → C(灰色)  
  3. 持续处理

    重复上述步骤,直到没有灰色对象存在:

    • 处理 B → B 的子对象(如有)标记为灰色,B 变黑
    • 处理 C → C 的子对象(如有)标记为灰色,C 变黑
    text 复制代码
    A(黑色) → B(黑色)  
    A → C(黑色)  
  4. 回收阶段

    所有白色对象视为不可达,被释放。


3. 增量标记的优势

传统标记是 全停顿(Stop-The-World) 的,而三色标记允许将标记过程拆分为多个小步骤:

  • 每次仅处理部分灰色对象
  • 主线程可以在标记间隙执行 JavaScript 代码
  • 显著减少单次阻塞时间(从几百毫秒 → 几毫秒)

写屏障(Write Barrier)

1. 问题背景

在增量或并发标记过程中,若主线程修改了对象引用关系,可能导致两种错误:

  • 漏标:本应存活的对象被错误回收(致命错误)
  • 多标:本应回收的对象未被回收(内存泄漏,可容忍)

示例场景

javascript 复制代码
// 初始状态:A 已标记为黑色(完成状态)
A.black = true;

// 主线程执行代码,将 A 的子引用从 B 改为 C
A.child = C;  // 原引用是 B

// 若 C 未被标记,且标记阶段已过 ➔ C 会被错误回收!

2. 写屏障的作用

写屏障是一种 拦截机制 ,当 JavaScript 代码修改对象引用时,触发额外的逻辑,确保标记的正确性。

V8 的实现方式是:在写入对象属性时,检查是否符合特定条件,并将目标对象标记为灰色。

关键规则

若一个黑色对象(已标记完成)新增了一个白色对象的引用,则将该白色对象标记为灰色。


3. 写屏障的伪代码实现

javascript 复制代码
// 当执行 obj.field = value 时
function writeBarrier(obj, field, value) {
  // 记录旧值(某些算法需要处理旧引用)
  const oldValue = obj[field];
  
  // 写入新值
  obj[field] = value;

  // 触发屏障逻辑
  if (isMarkingInProgress() &&       // 当前处于标记阶段
      isBlack(obj) &&                // 写入者(obj)是黑色
      isWhite(value)) {              // 被写入的值(value)是白色
    markGray(value);                 // 将 value 标记为灰色
  }
}

总结

阶段 技术手段 目标 适用场景
分代内存 新生代 + 老生代 针对性优化回收效率 所有对象
新生代回收 Scavenge 算法 快速回收短期对象 小对象、高分配率
老生代回收 标记-清除/整理 处理长期存活对象 大对象、低死亡率
增量标记 三色标记 + 写屏障 减少主线程阻塞 大型应用、低延迟需求
并发回收 多线程并行 最大化利用 CPU 资源 多核环境
相关推荐
前端御书房2 分钟前
基于 Trae 的超轻量级前端架构设计与性能优化实践
前端·性能优化
南部余额8 分钟前
playwright解决重复登录问题,通过pytest夹具自动读取storage_state用户状态信息
前端·爬虫·python·ui·pytest·pylawright
前端与小赵18 分钟前
webpack和vite之间的区别
前端·webpack·vite
zy01010121 分钟前
React受控表单绑定
前端·javascript·react.js·mvvm·双向数据绑定
百锦再22 分钟前
React编程的核心概念:数据流与观察者模式
前端·javascript·vue.js·观察者模式·react.js·前端框架·ecmascript
2401_8724878824 分钟前
网络安全之前端学习(css篇2)
前端·css·学习
SuperYing34 分钟前
前端候选人突围指南:让面试官主动追着要简历的五大特质(个人总结版)
前端·面试
前端双越老师37 分钟前
我的编程经验与认知
前端
linweidong43 分钟前
前端Three.js面试题及参考答案
前端·javascript·vue.js·typescript·前端框架·three.js·前端面经
GISer_Jing1 小时前
前端常问的宏观“大”问题详解(二)
linux·前端·ubuntu