标记清除是V8老生代核心回收机制:先暂停JS执行,从根出发可达性分析标记存活对象;再扫描释放未标记内存,虽高效但产生碎片,需Mark-Compact按需整理。

标记清除(Mark-and-Sweep)是 V8 引擎老生代内存回收的底层核心机制,它不依赖引用计数,而是基于可达性分析判断对象生死:只要一个对象能从根(如全局对象、栈中变量、DOM 节点等)出发,沿引用链被访问到,它就算“活着”;否则就是垃圾,会被回收。
标记阶段:从根出发,递归打标
GC 启动后,先暂停 JavaScript 执行(Stop-The-World),然后从所有根对象开始,深度优先或广度优先遍历整个对象图,对每个可达对象打上“存活”标记。这个过程覆盖所有可能被程序用到的对象,包括嵌套属性、闭包捕获的变量、甚至跨 iframe 或 Worker 的强引用目标。
- V8 实际采用增量标记——把大标记任务拆成多个微任务,穿插在 JS 执行间隙中运行,大幅缩短单次停顿时间
- 标记期间若 JS 修改了引用关系(比如赋值、删除属性),V8 会通过写屏障(write barrier)记录变更,确保标记结果准确
- 循环引用对象(如
a.b = b; b.a = a)只要整体不可达,就不会被标记,自然进入回收队列
清除阶段:扫描堆内存,释放未标区域
标记完成后,GC 线性扫描老生代整个堆内存空间,识别出所有未被标记的内存块,直接将其地址加入空闲列表(free list),供后续 new 对象分配使用。
- 这一步不移动任何存活对象,只做逻辑清理,所以速度快、开销低
- 但副作用明显:多次清除后,空闲内存变成细碎的“小块”,难以满足大对象分配需求
- V8 不会立刻整理,而是等碎片严重到影响分配成功率时,才触发更重的 Mark-Compact
为什么老生代不用 Scavenge?
Scavenge 是新生代用的复制算法,适合小空间、高死亡率场景。而老生代对象体积大、数量多、存活率常超 80%,如果强行复制:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
- 每次 GC 都要搬运大量数据,CPU 和内存带宽压力剧增
- 复制过程需双倍空间冗余,老生代动辄几百 MB,成本不可接受
- 对象地址频繁变动,会破坏弱引用(WeakMap/WeakRef)和调试器稳定性
碎片多了怎么补救?Mark-Compact 上场
当空闲列表里最大连续块太小,无法分配新对象时,V8 会启动 Mark-Compact:
- 先执行一次完整 Mark(复用已有标记结果)
- 再将所有存活对象按顺序紧凑地挪到堆内存一端(比如起始位置)
- 最后清空尾部全部内存,形成一大片连续可用空间
- 代价是耗时更长、暂停更久,所以只在必要时触发,不是每次 Mark-Sweep 都跟上
不复杂但容易忽略:Mark-Sweep 本身不解决碎片,它专注“精准识别+快速释放”;碎片治理是另一层策略,靠 Mark-Compact 按需兜底。理解这点,就能明白为什么监控内存时看到“已回收但没明显下降”——那可能是碎片还在,等着下一次整理。

















