JavaScript 语言性能优化

说明: 具体从内存空间的使用以及垃圾回收机制的角度出发。

内存管理

  • 为什么要进行内存管理


    内存.png

    内存管理是可以避免程序出现一些不可察觉的内存问题,比如内存泄漏,当这些问题反复出现在代码中就会有意想不到的bug

  • 内存管理
    • 内存:可读写的单员组成,表示一片操作空间
    • 管理:操作一片空间的申请,使用,释放

申请空间-使用空间-释放空间

// 申请空间
let obj = { name: 'glh' };
// 使用空间
let ali = obj;
// 释放空间
obj = null;

JavaScript中的垃圾

  • JavaScript 中的内存管理是自动的
  • 对象不再被引用时被看作是垃圾
  • 对象不能从根上访问
可达对象
  • 从跟上出发可以访问到的对象就是可达对象(引用,作用域链)
  • JavaScript的根可以理解为全局变量

GC 算法

  • GC 就是垃圾回收机制的简写
  • GC可以找到内存中的垃圾,并释放和回收空间

所谓的GC算法就是垃圾回收器工作的时候查找和回收所遵循的规则

常见的GC算法
  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

引用计数

在内部维护一个引用计数器,给每个对象设置引用数,当引用关系发生变化时,判断当前对象引用数是否为 0,当为 0 时候,当前对象为垃圾对象,被 GC 回收且释放内存空间

  • 优点:1,发现垃圾对象,立即回收;2,最大限度减少程序暂停。

  • 缺点:1,无法回收循环引用的对象;2,时间开销大

function(){
  const obj1 = {}
  const obj2 = {}
  obj1.name =  obj2
  obj2.name = obj1  
   return ''
}
f()

以上代码在采用引用计数算法进行垃圾回收时,并不会对obj1和obj2进行回收,因为互相存在引用

如果一个对象的引用数为 0,则该对象将被回收。程序运行时,对内存的消耗除逻辑代码外,也包括了 GC 算法的消耗,引用计数固然可以在程序出现垃圾的时候可以及时回收释放内存,也因内存可以不断得到释放而减少了程序暂停的时间,但是 GC 同时也需要维护‘roots’表来统计引用计数,当代码中引用较多时,也会带来损耗。同时对于 对象之间互有引用的情况,即使对象本身没有被使用,但是引用存在就导致了引用计数不为 0,无法被回收的情况。

标记清除算法

  • 将标记和清除分为两个阶段
  • 遍历所有对象找到所有活动对象标记
  • 遍历所有对象清除没有标记的对象
  • 回收相应的空间

标记清除算法会递归的寻找对象之间的引用获取所有可达对象,并为其做上标记, 但是标记清除算法所回收的内存地址在内存上不一定是连续的,这就导致了内存空间的碎片化(类似磁盘碎片), 浪费空间,并且标记清除法是不会立即回收对象的,而且当标记清除算法运行的时候,程序会被暂停

优点:循环引用的对象也会被回收 缺点:不会立即回收垃圾对象

标记整理算法

标记整理算法增强了标记清除算法,遍历所有内存对象,将所有可达内存对象标记,其在回收非活动对象前会将对象的地址进行移动,使其在地址上连续,然后再回收。

优点:不会存在内存空间的碎片化 缺点:不会立即回收垃圾对象

V8 简介

  • V8引擎是一款主流的JavaScript执行引擎
  • 采用即使编辑,代码可以直接转成机器码运行
  • V8 的运行内存上限是 1.5G(32 位系统为 800M)

由于 V8 的运行内存是有上限的,因此垃圾回收需要使用分代回收算法,然后针对于新/老生代对象采取不同的 GC 算法

V8中常用的GC算法
  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量
V8的垃圾回收策略
  • V8 内存一分为二(新生代,老生代)
  • 针对不同代对象采用不同GC算法


    回收策略.png
新生代对象的回收操作
  • 小空间用于存储新声代对象 32M(32 位为 16M)

  • 新生代对象指存活时间较短对象(局部作用域的对象,经过一轮 GC 就会被回收的对象)

  • 新生代对象采用采用复制算法 + 标记整理进行回收

  • 新生代内存区同样会被一分为二(等大小 From & To)

  • 活动对象存储在From空间内,当 From 空间应用到一定大小的时候就会触发GC操作使用标记整理并整理活动对象的地址,使其连续然后将活动对象拷贝至 To,然后 From 空间进行内存释放。

  • 当完成一次 GC 操作之后,From 和 To 需要进行置换。

细节说明:

  • 在从FromTo拷贝的过程中有可能出现变量晋升的情况,变量晋升就是新生代的对象移动到老生代。

    晋升条件:

    • 一轮 GC 执行完毕之后还存活的新生代则需要晋升。

    • 当 To 空间的使用率超过 25%的时候,同样需要将此次的活动对象均移动到老生代中。

老生代对象的回收操作
  • 老生代区域大小约 1.4G(32 位大小为 700M),老生代存放的对象为存活时间较长的对象,一般为 window 下的变量或被闭包保存的变量。

  • 老生代区域采用标记清除,标记整理,增量标记的 GC 算法。首先使用标记清除完成对垃圾空间的回收,当新生代区域出现晋升现象时,如果老生代空间不足,则会使用标记整理进行空间优化。同时在老生代变量进行标记的时候也会采用增量标记算法进行效率优化。

  • 增量标记是对标记清除算法的优化,让其不会一口气的去寻找到所有活动对象。而是会穿插在程序的运行中执行,降低了程序的卡顿,当标记彻底采集完毕之后,才会把程序停下来,进行垃圾回收。

新老生代垃圾回收细节对比
  • 新生代区域,采用复制算法, 因此其每时每刻内部都有空闲空间的存在(为了完成 From 到 To 的对象复制),但是新生代区域空间较小(32M)且被一分为二,所以这种空间上的浪费也是比较微不足道的。

  • 老生代因其空间较大(1.4G),如果同样采用一分为二的做法则对空间大小是比较浪费,且老生代空间较大,存放对对象也较多,如果进行复制算法,则其消耗对时间也会更大。也就是是否使用复制算法来进行垃圾回收,是一个时间 T 关于内存大小的关系,当内存较小时,使用复制算法消耗的时间是比较短的,而当内存较大时,采用复制算法对时间对消耗也就更大。

内存问题的外在表现
  • 页面出现延迟加载或经常性暂停: 可能存在频繁当GC操作,存在一些代码瞬间吃满了内存。
  • 页面出现持续性的糟糕性能: 程序为了达到最优的运行速度,向内存申请了一片较大的内存空间,但空间大小超过了设备所能提供的大小。
  • 页面使用随着时间延长越来越卡: 可能存在内存泄漏。
使用任务管理器查看内存

唤起浏览器自带的任务管理器,观察 js 内存,如果 js 内存在持续增大,则存在内存问题。

Timeline 记录内存

打开 Timeline 开始录制,进行页面操作,结束录制之后,开启内存勾选,拖动截图到指定时间段查看发生内存问题时候到页面展示,并定位问题。同时可以查看对应出现红点到执行脚本,定位问题代码。

利用浏览器控制台内存模块,查找分离 dom

在页面上进行相关操作后,进行“拍照”,在快照中查找Detached HTMLElement,回到代码中查找对应的分离 dom 存在的代码,在相关操作代码之后,对分离 dom 进行释放,防止内存泄漏。

如何确定频繁的垃圾回收操作
  • GC 工作时,程序是暂停的,频繁/过长的 GC 会导致程序假死,用户会感知到卡顿。
  • 查看 Timeline 中是否存在内存走向在短时间内频繁上升下降的区域。浏览器任务管理器是否频繁的增加减少。

代码优化

慎用全局变量
  • 全局变量定义在全局执行的上下文,是所有作用域链的顶端

  • 全局执行上下文一直存在于上下文执行栈,直到程序退出

  • 如果某个局部作用域出现了同名变量则会屏蔽或者污染全局作用域

  • 全局变量的执行速度,访问速度要低于局部变量,因此对于一些需要经常访问的全局变量可以在局部作用域中进行缓存

一些性能上的小问题
  • 通过原型对象添加方法与直接在对象上添加成员方法相比,原型对象上的属性访问速度较快。

  • 当回调函数可以单独抽离的时候,其执行速度要较快。

  • 直接访问属性,会比通过方法访问属性速度来的快。

  • loop 遍历速度 forEach > 优化 for > for in

  • 节点克隆(cloneNode)生成节点速度要快于创建节点。

  • 字面量声明的数据生成速度要快于单独属性赋值行为生成的数据。
    ......

你可能感兴趣的:(JavaScript 语言性能优化)