跳到主要内容

垃圾回收

垃圾回收在做什么?​

  • 语义: 引擎自动释放不再使用的内存;
  • 目标: 回收不可达的变量与对象, 无需手动释放;

内存对象如何分代?​

  • 新生代: 寿命较短的对象;
  • 老生代: 经过多次回收仍存活的对象;

各代使用什么回收算法?​

算法过程
停止-复制式把堆分为活动与空闲两部分, 暂停执行, 把存活对象复制到空闲空间并整理压缩, 再交换两部分角色, 清理旧空间
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 代替删除属性;