垃圾回收
垃圾回收在做什么?
- 语义: 引擎自动释放不再使用的内存;
- 目标: 回收不可达的变量与对象, 无需手动释放;
内存对象如何分代?
- 新生代: 寿命较短的对象;
- 老生代: 经过多次回收仍存活的对象;
各代使用什么回收算法?
| 算法 | 过程 |
|---|---|
| 停止-复制式 | 把堆分为活动与空闲两部分, 暂停执行, 把存活对象复制到空闲空间并整理压缩, 再交换两部分角色, 清理旧空间 |
| mark-and-sweep | 标记所有存活对象, 再清理未标记对象 |
| mark-and-compact | 标记存活对象后把老生代对象压缩到内存一侧, 避免内存碎片 |
- 分工: 新生代用 Scavenge 算法 (即停止-复制式), 活动空间满时触发; 老生代组合使用 mark-and-sweep 与 mark-and-compact;
- 追踪起点: 从全局对象与栈上引用出发标记可达对象, 因此循环引用在追踪算法下也能被回收;
循环引用为什么曾经是问题?
- 旧口径: 引用计数式回收下, 两个对象互相引用就永远不会被释放;
- 现代口径: 追踪式标记从根可达性出发, 互相引用但都不可达的对象仍会被回收;
- 兜底: 仍可主动把不再需要的引用置为
null;
const objectA = {};
const objectB = {};
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
哪些写法会增加回收压力?
- 全局变量: 常驻内存, 寿命最长;
- 频繁创建大对象: 触发更多回收, 而回收需要暂停执行, 影响设备性能;
- 闭包: 延长外层变量对象的生命周期;
- hidden class 变化: V8 为对象维护 hidden class, 运行时增删属性会破坏共享结构;
如何降低回收代价?
- 及时释放: 不再使用时把引用置为
null; - 缩小作用域: 避免全局变量, 把变量定义在函数作用域内;
- 声明方式: 用
let/const代替var; - 闭包: 减少不必要的闭包与常驻引用;
- 对象结构: 预先设计好全部属性, 用赋值
null代替删除属性;