全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-15 10 分钟 ✍️ juanwangdev

垃圾回收机制

V8 GC 采用分代回收,根据对象存活时间采用不同策略,平衡效率与内存利用率。

分代回收原理

对象分代

C
+------------------+------------------+
|   New Space      |    Old Space     |
|   (新生代)        |    (老生代)       |
|   1-8MB          |    数十MB-GB      |
+------------------+------------------+
|   Scavenge       |  Mark-Sweep      |
|   复制算法        |  标记整理算法      |
+------------------+------------------+

对象生命周期

C
对象创建 → New Space 分配
    ↓
短期使用 → Scavenge GC 回收
    ↓
存活多次 → 晋升 Old Space
    ↓
长期使用 → Mark-Sweep GC 回收

分代假设

C
弱分代假设:大多数对象生命周期很短
强分代假设:存活越久的对象越可能继续存活

结论:频繁回收新生代,偶尔回收老生代

新生代 GC:Scavenge

Cheney 复制算法

C
New Space 分两半(Semispace):
+----------+----------+
|  From    |   To     |
|  (活跃)  |  (空闲)  |
+----------+----------+
各 1-4MB

GC 流程:
1. From 空间对象标记存活
2. 存活对象复制到 To 空间
3. 释放 From 空间全部对象
4. From/To 交换角色

复制过程

C
// Cheney 算法伪代码
void scavenge() {
  void* scan = to_space.start;
  void* free = to_space.start;

  // 复制 GC Roots 直接引用的对象
  copy_roots_to_to_space(&free);

  // 遍历 To 空间,扫描引用
  while (scan < free) {
    Object* obj = (Object*)scan;

    // 遍历对象的所有引用
    for (each reference ref in obj) {
      if (ref is in from_space) {
        // 复制到 To 空间
        copy_object(ref, &free);
      }
    }
    scan += obj->size;
  }

  // 交换 From/To
  swap(from_space, to_space);
}

对象晋升

JavaScript
晋升条件:
1. 对象已存活过一次 Scavenge
2. To 空间占用超过 25%

晋升流程:
From 空间存活对象 → 检查晋升条件 → 复制到 Old Space

Scavenge 特点

优点缺点
速度快(只扫描半空间)空间利用率低(50%)
无碎片(复制时整理)存活多时复制开销大
分配快(指针碰撞)大对象不适合

老生代 GC:Mark-Sweep-Compact

三阶段流程

Bash
Mark(标记):
  从 GC Roots 开始
  → 遍历所有可达对象
  → 标记为存活

Sweep(清除):
  遍历堆空间
  → 未标记对象回收
  → 加入空闲列表

Compact(整理):
  移动存活对象
  → 消除内存碎片
  → 更新引用地址

GC Roots

JavaScript
GC Roots 来源:
1. 全局对象(global)
2. 当前执行栈中的变量
3. 内部引用(builtins、handle scope)
4. 活跃的 V8 Handle

三色标记算法

Bash
// 对象颜色状态
WHITE: 未访问,GC 后回收
GRAY:  已访问,但引用未扫描完
BLACK: 已访问,引用已全部扫描

// 标记流程
void mark() {
  // 初始化:全部白色
  for (obj in heap) obj.color = WHITE;

  // GC Roots 加入灰队列
  for (root in roots) {
    root.color = GRAY;
    push(gray_queue, root);
  }

  // 处理灰色对象
  while (!empty(gray_queue)) {
    Object* obj = pop(gray_queue);

    // 扫描所有引用
    for (ref in obj.references) {
      if (ref.color == WHITE) {
        ref.color = GRAY;
        push(gray_queue, ref);
      }
    }

    // 标记完成
    obj.color = BLACK;
  }
}

// Sweep: 回收白色对象
void sweep() {
  for (obj in heap) {
    if (obj.color == WHITE) {
      free(obj);
    } else {
      obj.color = WHITE;  // 重置,下次 GC 使用
    }
  }
}

标记过程图示

JavaScript
初始状态:
  所有对象:白色

第一步:
  GC Roots → 灰色

第二步:
  Roots 引用 → 灰色
  Roots → 黑色

第三步:
  继续扫描灰色
  直到无灰色对象

最终:
  黑色:存活
  白色:回收

Compact 整理

JavaScript
// 内存碎片整理
void compact() {
  void* free = heap_start;

  // 遍历存活对象
  for (obj in heap) {
    if (obj.is_live) {
      // 移动到连续位置
      memmove(free, obj, obj->size);
      update_references(obj, free);
      free += obj->size;
    }
  }

  // 更新空闲边界
  heap_top = free;
}

增量标记

问题背景

JavaScript
传统 Mark-Sweep:
一次性完成,暂停时间长

大堆场景:
几 GB 堆 → 标记需要几百毫秒 → 请求延迟

增量标记原理

JavaScript
将标记分成多个小步骤:
JS执行 ── 标记1 ── JS执行 ── 标记2 ── JS执行 ── Sweep

每次只标记一小部分,减少单次停顿

Write Barrier

JavaScript
// 增量标记时 JS 可能修改引用
// 需要 Write Barrier 同步

void write_barrier(Object* obj, Object* new_ref) {
  if (obj.color == BLACK && new_ref.color == WHITE) {
    // 黑色对象引用白色对象
    // 必须将白色对象变灰
    new_ref.color = GRAY;
    push(gray_queue, new_ref);
  }

  // 更新引用
  obj.ref = new_ref;
}

增量标记流程

text
1. 标记一部分 → 暂停
2. JS 执行一段时间
3. Write Barrier 记录修改
4. 继续标记剩余部分
5. 重复直到标记完成
6. 最终 Sweep(仍需全停顿)

并发与并行 GC

并行 GC

text
主线程 + 辅助线程同时 GC
利用多核 CPU 加速

┌─────────────────────────────────────┐
│ Main │ GC │ GC │ GC │ GC │ GC │     │
│      │───────────────────────────── │
│      │ Helper1 │ Helper2 │ Helper3 │
└─────────────────────────────────────┘

适用:Scavenge、Compact

并发 GC

text
GC 在后台线程运行
主线程几乎不停顿

┌─────────────────────────────────────┐
│ Main │ JS │ JS │ JS │ JS │ JS │     │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ GC Thread │ Mark │ Mark │ Mark │    │
└─────────────────────────────────────┘

需要 Write Barrier 同步
V8 6.0+ 老生代标记使用并发

三种模式对比

模式主线程停顿CPU 利用适用
全停顿单核小堆
增量多次短停顿单核中堆
并发极短多核大堆

GC 触发时机

新生代触发

text
触发条件:From 空间分配失败

频率:高频(毫秒级)
原因:空间小(1-8MB),很快填满

老生代触发

text
// 触发阈值
static const double kOldSpaceGrowingFactor = 1.5;

// 当已用内存达到限制的 1/1.5 时触发
if (used_size > limit / 1.5) {
  schedule_gc();
}

手动触发

text
// --expose_gc 参数启用
node --expose_gc app.js

global.gc();  // 手动触发 Full GC

// 不推荐生产使用

GC 参数配置

堆大小参数

text
# 老生代上限
--max-old-space-size=4096  # 4GB

# 新生代半空间大小
--max-semi-space-size=8    # 8MB

# 堆总上限
--max-heap-size=4096

查看默认值

text
const v8 = require('v8');
const stats = v8.getHeapStatistics();

console.log('Heap Limit:', stats.heap_size_limit / 1024 / 1024, 'MB');
// 64位:约 1.4GB
// 32位:约 700MB

GC 日志分析

启用 GC 日志

text
# 打印 GC 信息
node --trace-gc app.js

# 详细日志
node --trace-gc --trace-gc-verbose app.js

日志解读

text
[12345:0x123] Scavenge 1.2 (3.4) -> 0.8 (4.0) MB, 2.5 ms

解读:
- Scavenge:新生代 GC
- 1.2 MB:回收前已用内存
- 3.4 MB:回收前总分配
- 0.8 MB:回收后已用
- 4.0 MB:回收后总分配
- 2.5 ms:GC 耗时

[12345:0x123] Mark-sweep 100.2 (150.0) -> 80.5 (150.0) MB, 50.3 ms

解读:
- Mark-sweep:老生代 GC
- 回收 19.7 MB
- 耗时 50.3 ms

GC 监控代码

Performance Observer

text
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach(entry => {
    console.log(`GC ${entry.kind}: ${entry.duration.toFixed(2)}ms`);
  });
});
obs.observe({ entryTypes: ['gc'] });

GC 统计

text
let gcStats = { minor: 0, major: 0, totalMs: 0 };

const obs = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    gcStats[entry.kind]++;
    gcStats.totalMs += entry.duration;
  });
});
obs.observe({ entryTypes: ['gc'] });

setInterval(() => {
  console.log('GC Stats:', gcStats);
  console.log('GC %:', (gcStats.totalMs / 60000 * 100).toFixed(2) + '%');
}, 60000);

减少 GC 压力

对象复用

text
// 减少:循环内创建对象
for (let i = 0; i < 10000; i++) {
  const obj = { id: i };  // 每次新建
}

// 优化:复用对象
const obj = { id: 0 };
for (let i = 0; i < 10000; i++) {
  obj.id = i;  // 修改属性
}

预分配

text
// 减少:数组动态增长
const arr = [];
for (let i = 0; i < 10000; i++) {
  arr.push(i);  // 多次扩容
}

// 优化:预分配大小
const arr = new Array(10000);
for (let i = 0; i < 10000; i++) {
  arr[i] = i;
}

及时释放

text
// 减少:引用未释放
let cache = loadData();
processData(cache);
// cache 仍被引用

// 优化:主动释放
let cache = loadData();
processData(cache);
cache = null;  // 允许 GC

GC 健康指标

指标健康需关注问题
minor GC 频率几十次/分钟几百次每秒多次
major GC 频率几次/分钟十几次几十次/分钟
GC 时间占比< 5%5-10%> 10%
单次 major GC< 50ms50-200ms> 200ms

注意:频繁 major GC 表明内存不足或存在泄漏,应检查堆使用和对象生命周期。

要点总结

  • 新生代用 Scavenge 复制算法,快速但空间利用率 50%
  • 老生代用 Mark-Sweep-Compact,三色标记、分步整理
  • 增量/并发标记减少 GC 停顿,Write Barrier 同步 JS 修改
  • 对象存活多次晋升老生代,To 空间超 25% 也晋升
  • 代码层面减少临时对象、预分配、及时释放引用

📝 发现内容有误?点击此处直接编辑

← 上一篇 libuv工作原理
下一篇 → 网络编程底层实现
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库