内存优化

Misaka10032内存管理垃圾回收JS代码优化大约 5 分钟

垃圾回收

JS 中的内存管理遵循:申请内存空间 -> 使用内存空间 -> 释放内存空间 的顺序

垃圾回收机制:

  1. JS 自动进行内存管理
  2. 对象不再被引用时就是垃圾
  3. 对象不能从根(全局变量对象)上访问的时候就是垃圾

下面的垃圾回收算法都按照这个机制执行垃圾回收

垃圾回收(GC)算法

GC 可以找到内存中的垃圾,并释放和回收空间

优点:

  • 发现垃圾立即回收
  • 最大限度地减少程序暂停

缺点:

  • 循环引用对象无法回收
  • 时间开销大(时刻维护对象的引用)

常用的 GC 算法如下。

引用计数

维护一个引用计数器,判断当前引用数是否为 0

引用数字为 0 的时候立即回收

互相引用的对象无法被回收

优缺点:

  • 可以即时回收垃圾对象
  • 减少程序卡顿时间
  • 无法回收循环引用的对象
  • 资源消耗较大

标记清除

分标记阶段和清除阶段两个阶段

遍历所有对象找标记活动对象

遍历所有对象清除没有标记对象

回收相应空间

优缺点:

  • 相对于引用计数,可以回收循环引用但没有用处的对象
  • 内存空间地址不连续、空间碎片化,由于回收的对象在本来的地址也是不连续的,就会导致回收的对象地址也是不连续的,浪费空间
  • 不会立即回收垃圾对象

标记整理

标记清除的增强版

标记阶段的操作和标记清除一致

清除阶段会先执行整理,移动对象位置,重点就是这个整理,标记清除就是因为回收的对象的地址不连续,导致碎片空间化。而标记整理会在清除前,先整理一遍,将活动的对象移动到一起,需要回收的对象移动到一起,这样回收的时候,回收对象的地址就是连续的。

优缺点:

  • 减少碎片化空间
  • 不会立即回收垃圾对象

V8 垃圾回收策略

V8 引擎采用的是分代回收

内存分为新生代存储区和老生代存储区,针对不同的对象采用不同的算法

V8 中常用的 GC:分代回收、空间复制、标记清除、标记整理、标记增量

新生代处理

V8 将内存空间一分为二

小空间用于存储新生代对象(32M | 16M)

新生代对象指的是存活时间较短的对象

新生代对象回收实现

回收过程采用复制算法+标记整理

新生代内存区分为两个等大小空间

使用空间为 from,空闲空间为 to

活动对象存储于 from 空间

标记整理后将活动对象拷贝至 to 空间

from 与 to 交换空间完成释放

回收细节说明

拷贝过程中可能出现晋升

晋升就是将新生代对象移动至老生代

一轮 GC 还存活的新生代就需要晋升

to 空间的使用率超过 25%

老生代处理

老生代对象存放在右侧老生代区域

64 位操作系统 1.4G,32 位操作系统 700M

老生代对象指的是存活时间较长的对象

老生代对象回收实现

主要采用标记清除,标记整理,增量标记算法

首先使用标记清除完成垃圾空间的回收(空间碎片)

采用标记整理进行空间优化(晋升的时候,使用标记整理,进行空间优化)

采用增量标记进行效率优化

新老生代回收细节对比

新生代区域垃圾回收使用空间换时间

老生代其余回收不适合复制算法(数量多、空间大)

增量标记优化

标记阶段的时候,会阻塞 JS 执行,而增量标记就是将标记的过程,分为几个小标记分段回收,以优化用户体验

总结

V8 采用即时编译、内存设限,使用分代回收思想实现垃圾回收

V8 中的内存分为新生代和老生代

V8 常见的 GC 算法:

  • 新生代:复制算法+标记整理
  • 老生代:标记清除,标记整理,增量标记算法

JS 代码优化

  • 慎用全局变量:全局变量定义在全局执行上下文,是所有作用域的顶端,而且全局上下文会一直存在上下文执行栈,直到程序退出,还会导致局部变量遮蔽污染。
  • 缓存全局变量,比如缓存 dom 节点,避免频繁获取
  • 通过原型新增方法
  • 避免属性访问方法使用,就是对象直接获取属性就行,不用封装方法获取属性。
  • for 循环遍历的时候,如果是数组长度,可以提前使用变量保存,不用每次循环都去获取。
  • 遍历一个数组,forEach 的性能比 for 和 for in 好。优化后的 for(数组长度提前读取)的性能比 for in 好。
  • 节点添加优化,使用文档碎片(document.createDocumentFragment)添加节点,等节点添加完毕之后再统一加入到 dom 上。
  • 克隆节点比创建节点性能更高
  • 直接量替换 Object 操作,比如数组,const a = [1,2,3]替换 const a = new Array(1,2,3)
  • 减少判断层级,if else 中,退出的情况应该放在最前判断,复杂层级深的情况应该放在最后
  • 减少作用域链查找层级
  • 减少数据读取次数,比如 dom 读取,可以先缓存。
  • 减少声明及语句数
  • 采用事件委托代替多个事件注册。