JavaScript性能优化
什么属于性能优化
1.提高运行效率的行为
2.降低运行开销的行为
学习点
- 内存管理
- 垃圾回收(GC:Garbage Collection)及一些算法
- V8引擎垃圾回收
- 内存监控:chrome开发工具Performance
内存管理
- 内存:由可读写单元组成,表示一片可操作空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
JavaScript中的内存管理
EcmaScript并没有提供操作内存的API,所以JS不能像其他语言(C、C++)一样,由开发者主动调用API,来完成空间的管理(申请、使用、释放操作)。
依然可以演示内存空间的声明周期
// 申请
// JS自动为变量分配内存空间
let obj = {}
// 使用
// 对变量的读写操作
obj.name = 'Jack'
// 释放
// 利用JS垃圾回收机制完成释放
obj = null
注:上例obj
会被垃圾回收机制(GC)自动回收,当因为特殊调用而未被GC回收时,可通过=null
对其进行释放。
JavaScript垃圾回收
- JS中内存管理是自动的
JavaScript中的垃圾
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
根
JavaScript中的根可以理解为是全局变量对象或全局的执行上下文环境
可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发,是否能够被找到
引用和可达
let obj = {
name: 'Jack'
}
// {name: 'Jack'}被obj引用,当前引用次数1
// global.obj是可达的
// global.obj.name是可达的
let ali = obj
// {name: 'Jack'}被ali引用,当前引用次数2
// global.ali是可达的
// global.ali.name是可达的
obj = null
// {name: 'Jack'}减少一次引用,当前引用次数1
GC算法
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间
- 算法就是工作时查找和回收所遵循的规则
常见GC算法
- 引用计数
- 标记清除
- 标记整理(V8)
- 分代回收(V8)
引用计数
- 核心思想:设置引用数,判断当前引用数是否为0
- 引用计数器:引用关系改变时修改引用数字
- 引用数字为0时立即回收
优点
- 监听对象引用数,发现垃圾时立即回收
- 最大限度减少程序暂停(当内存被占满时,GC就会立即寻找垃圾进行释放)
缺点
- 无法回收循环引用的对象
function fn () {
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
}
fn()
// 根据引用计数算法,obj1和obj2由于循环引用,引用数值永远不为0,GC无法回收obj1和obj2
- 资源开销大、耗时(引用计数需要监控维护引用数值的变化,对象越多,需要维护的数值也越多,相比其他GC算法更耗时)
标记清除
- 核心思想:标记和清除两个阶段
- 标记:遍历堆内存中的所有对象,(从根开始)找到活动对象(可达对象),对其进行标记
- 清除:遍历堆内存中的所有对象,清除没有标记的对象,然后清除所有的标记(下次GC工作时重新标记)
- 触发:当程序运行期间,若可以使用的内存被耗尽时,GC线程就会将程序暂停开始工作,先将依旧存活的对象标记一遍,再将堆中没有被标记的对象全部清除,最后让程序恢复运行。
function fn () {
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
}
fn()
// 假设此时内存被占满/耗尽,触发GC工作
// 引用计数判断obj1和obj2还在被引用,无法回收
// 标记清除从根上访问不到obj1和obj2,不对其标记,清除阶段回收
// 上面所说的obj1和obj2指的是它们所指向的存储在堆内存中的数据
优点
- 相对引用计数,解决对象循环引用不能回收的问题
缺点
- 空间碎片化:标记清除回收的内存空间,在地址上是不连续的,分散在各个角落。不能让空间最大化的使用
- 由于不连续,GC会将回收的内存单元存放到一个空闲内存列表中,对这个列表的维护也是一种开销。
- 不会立即回收垃圾对象,工作时程序是暂停的
标记整理
- 标记清除的增强,减少碎片化空间
- 不同于标记清除,标记整理在清除阶段,会先整理移动对象的位置,让它们在地址上产生连续,然后再进行回收
- 不会立即回收垃圾对象,工作时程序是暂停的
V8
- 主流的JavaScript执行引擎,内存管理机制,高效
- V8采用即时编译
- V8内存设限:64位操作系统不超过1.5G,32位操作系统不超过800M
- 对于web应用来说足够
- 这个上限使垃圾回收时,不会令回收时间超过用户的感知
原始类型数据由JS语言自身管理。
存放在堆区的对象类型数据由内存进行管理,GC进行回收。
V8常用GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 增量标记
V8分代回收
V8内存空间是有上限的,所以它按照一定规则分成两个空间,分别存放新生代和老生代对象,针对不同代采用适合的GC算法
新生代
- 小空间存储新生代对象,空间大小:64位32M,32位16M
- 新生代指的是存活时间较短的对象,例如局部变量,在函数执行完就会被回收
新生代对象回收实现
回收过程采用复制算法+标记整理
- 新生代内存也会区分为两个等大的空间,分别为使用空间From、空闲空间To
- 为活动对象分配空间时,先存储于From空间
- 当From空间应用的一定的程度(占满)之后,就会触发GC操作
- 使用标记整理的算法对活动对象进行标记和整理
- 然后将被标记的对象拷贝至To空间
- 回收时清空From,From与To进行调换,完成释放
晋升
新生代对象满足一定标准就会被晋升(晋升就是将对象移至老生代):
- 一轮GC还存活的新生代需要晋升
- 拷贝过程中,To空间的使用率超过25%,就会晋升之后拷贝的活动对象
新生代的意义
优化垃圾回收(GC)的性能
- 简化了新对象的分配(只在新生代分配内存)
- 可以更有效的清除不再需要的对象(新老生代使用不同的GC算法)
研究发现:
- 很多对象的生存时间都很短。
- 新生对象很少引用生存时间长的对象。
结合以上两点,很明显GC会频繁访问新生对象,例如新生代空间。在新生代中,GC可以快速标记回收“死对象”,而不需要扫描整个Heap中的存活一段时间的“老对象”。
老生代
- 空间大小:64位1.4G,32位700M
- 老生代对象指存活时间较长的对象,例如全局变量、闭包
老生代对象回收实现
采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 当新生代对象晋升时(对象从新生代移动到老生代),老生代中没有足够的空间去存放,就会触发标记整理对碎片化的空间进行整理回收。
- 采用增量标记进行效率优化
增量标记
GC触发标记清除回收时,程序会暂停,标记清除算法一口气标记全部活动对象,相对耗时太久。
增量标记将标记过程根据直接可达对象和间接可达对象(例如obj.foo和obj.foo.bar),将一整个过程拆分成多个小过程,每个过程指标记一层可达对象,标记完继续执行程序,然后再执行下个标记小过程。
这样对用户来说,程序执行更连贯。
新生代 VS 老生代
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法(对象太多,复制耗时)
内存监控
监控工具 performance
performance是W3C引入的web API。接口可以获取当前页面中与性能相关的信息。
可以获取的信息:
- 白屏时间:从打开网站到有内容渲染出来的时间节点
- 首屏时间:首屏内容渲染完毕的时间节点
- 用户可操作的时间节点:domready触发节点
- 页面总下载时间:window.onload的触发节点
- DNS查询时间
- TCP链接时间
- …
使用:
- 可以通过只读属性
window.performance
获得 - 可以通过浏览器开发者工具使用
更多查看监控工具 performance
内存问题的体现
- 页面的性能随时间延长越来越差(内存泄漏)
- 页面持续性出现糟糕的性能(内存膨胀)
- 页面出现延迟加载或经常性暂停(频繁垃圾回收)
界定内存问题的标准
- 内存泄漏:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 当前应用程序本身为了达到最优的效果,需要分配很大的内存空间。也许由于硬件不支持造成。需要在多个设备上测试,判断是程序问题还是设备问题。
- 频繁垃圾回收:通过内存变化图进行分析
常见内存监控的方式
- 浏览器任务管理器
- Timeline时序图记录
- 堆快照查找分离DOM(分离DOM:内存泄漏)
- 借助不同工具获取当前内存的走势图,进行一个时间段的分析,从而判断是否存在频繁的垃圾回收
任务管理器监控内存
Shift+Esc调出浏览器自带的任务管理器
- 内存:DOM节点所占用的内存
- Javascript内存:JS使用的堆内存
- 括号中的内存:所有可达对象正在使用的内存
可达对象使用的内存不断增加,说明内存使用是有问题的。
Timeline 时间线记录内存变化
Heap Snapshot 堆快照查找分离DOM
堆快照留存JS堆照片
分离DOM
- 界面元素(DOM节点)存活在DOM树上,DOM节点存在两种形态:垃圾对象和分离DOM
- 垃圾对象:脱离了DOM树,并且JS中也没有被引用
- 分离状态的DOM节点:脱离了DOM树,但JS中还被引用。
分离DOM占据内存空间,这就是种内存泄漏,可通过堆快照查找分离DOM,从而在代码中进行清除。
判断是否存在频繁GC
为什么确认是否存在频繁垃圾回收
- GC工作时应用程序是停止的
- 频繁且过长的GC会导致应用假死
- 用户使用中感知应用卡顿
如何确认频繁的垃圾回收
- Timeline中频繁的上升下降
- 任务管理器中数据频繁的增加减小