垃圾回收机制
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 | < 50ms | 50-200ms | > 200ms |
注意:频繁 major GC 表明内存不足或存在泄漏,应检查堆使用和对象生命周期。
要点总结
- 新生代用 Scavenge 复制算法,快速但空间利用率 50%
- 老生代用 Mark-Sweep-Compact,三色标记、分步整理
- 增量/并发标记减少 GC 停顿,Write Barrier 同步 JS 修改
- 对象存活多次晋升老生代,To 空间超 25% 也晋升
- 代码层面减少临时对象、预分配、及时释放引用
📝 发现内容有误?点击此处直接编辑