概述
- JavaScript语言的优化
内存管理
- 内存:由可读写单元组成,表示一片可操作空间
- 管理:人为操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请-使用-释放
JavaScript中的内存管理
-
申请内存空间
let obj = [] -
使用内存空间
obj.name = 'lg' -
释放内存空间
obj = null
垃圾回收与常见GC算法
- 垃圾
- JavaScript中内存管理是自动的
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
- 可达对象
-
可以访问到的对象就是可达对象(引用、作用域链)
-
可达的标准就是从根出发是否能够被找到
-
JavaScript中的根就可以理解为是全局变量对象
let a = {name:'1'} let ali = a a = null //ali还在引用这个对象,1仍是可达的可达对象:
graph TB A[全局变量]--> |obj| B[Object : o1,o2] B[Object : o1,o2] --> |o1| C[Object name:obj1] B[Object : o1,o2] --> |o2| D[Object name:obj2] C[Object name:obj1] --> |Next| D[Object name:obj2] D[Object name:obj2] --> |Prev| C[Object name:obj1]
-
GC算法
- GC垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间
- GC里的垃圾
- 程序不再需要使用的对象
- 程序中不能再访问到的对象
- 例如:函数中定义的对象
- 算法就是工作时查找和回收所遵循的规则
常见GC算法
1.引用计数
-
核心思想:设置引用数,判断当前引用数是否为0
-
引用计数器
-
引用关系改变时修改引用字
-
引用数字为0时立即回收
-
const user1 = {age:11}
const user2 = {age:22}
const user3 = {age:33}
const nameList = [user1.age,user2.age,user3.age]
function fn() {
const num1 = 1
const num2 = 2
}
fn()
//执行完num1\2就引用不到了
//user1\2\3还在被引用着
- 优点:
- 发现垃圾时立即回收
- 最大限度减少程序暂停:当内存即将爆满时,引用计数立即找到那些引用计数为0的对象空间,对其进行释放
- 缺点:
-
无法回收循环引用的对象
function fn(){ const o1 = {} const o2 = {} o1.name = o2 o2.name = o1 //在作用域内还有一个互相的指引关系,引用计数器的数值不为0 //造成内存的浪费 return 0 } fn() -
时间开销大:维护一个数值的变化
2.标记清除
-
核心思想:分标记和清除二个阶段完成
-
(递归)遍历所有对象找标记活动对象
-
(递归)遍历所有对象清除没有标记对象
-
回收相应的空间:放在空闲列表上,方便之后的程序使用
-
优点:
解决循环中不可达对象无法收回的问题:回收的时候会直接找到没有引用标记的对象回收
-
缺点:
空间碎片化:地址不连续,申请地址空间大于、小于所拥有空间
3.标记整理
-
标记整理时标记清除的增强
-
标记阶段的操作和标记清除一致
-
清除阶段会先执行整理,移动对象位置:能让地址产生连续
-
优点:解决空间碎片化问题
-
缺点:不会立即回收垃圾对象
-
分代回收
V8引擎的垃圾回收
V8
- 一款主流的JavaScript执行引擎
- 采用即时编译:源码=>机器码
- 内存有限制:64位不超过1.4g,有垃圾回收的机制
- 基于分代回收思想实现垃圾回收
- 内存分为新生代和老生代
垃圾回收策略
V8常用的GC算法
-
分代回收思想:内存分为新生代、老生代,针对不同对象采用不同算法
-
空间复制
-
标记清除
-
标记整理
-
标记增量
新生代回收
新生代:存活时间较短的对象,例如,函数内变量对象
老生代:反之
V8内存分配
- V8内存空间一分为二
- 小空间用于存储新生代对象(32M | 16M)
算法实现
- 回收过程采用复制算法 + 标记整理
- 新生代内存区分为二个等大小空间
- 使用空间为From,空闲空间为To
- 活动对象存储于From空间
- 标记整理后将活动对象拷贝至To
- From与To交换空间完成释放
注意:
-
拷贝过程可能出现晋升:将新生代对象移动至老对象
什么时候晋升:一轮GC还存活的新生代需要晋升
-
To空间的使用率超过25%
老生代回收
- 存放在右侧老生代区域
- 64位操作系统1.4G,32操作系统700M
算法实现
- 采用标记清除 + 标记整理 + 增量标记算法
- 使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化(晋升触发)
- 采用增量标记进行效率优化
对比:
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法
采用增量标记进行效率优化:
※ 标记可以不一口气做完:直接可达和间接可达。找到第一层可达就可以停下去让程序执行一会儿,再让GV做二步执行操作……反复,最后清除
Performance工具
为什么使用Performance
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用,时刻关注确定合理
- Performance提供多种监控方式
使用步骤
内存问题的外在表现
- 页面出现延迟加载或者经常性暂停
- 页面持续性出现糟糕的性能
- 页面的性能随着时间延长越来越差
监控内存的几种方式
界定内存问题标准
-
内存泄漏:内存使用持续升高
-
内存膨胀:在多数设备上都存在性能问题
-
频繁垃圾回收:通过内存变化图进行分析
监控方式
1.浏览器任务管理器
shift+esc
内存:DOM节点占用的内存,不变为好
JavaScript内存:小括号内不变为好
2.Timeline时序图记录
下降:垃圾回收
3.对快照查找分离DOM
-
界面元素存活在DOM树上
-
垃圾对象时的DOM节点:节点从DOM树上脱离,也没有引用的DOM节点
-
分离状态的DOM节点:从DOM树上分离,但是还在引用
清除分离DOM:当不使用节点时置null
- 判断是否存在频繁的垃圾回收
代码优化实例
代码优化介绍
精准测试JavaScript性能
-
本质:采集大量的执行样本进行数学统计和分析
-
基于Benchmark.js的jsperf.com
Jsperf使用:
-
填写详细的测试用例信息(title、slug)
-
填写准备代码(DOM操作时经常使用)
-
填写必要setup与teardown代码
-
填写测试代码片段
-
慎用全局变量
- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出
- 某个局部作用域出现了同名变量则会遮蔽或污染全局
缓存全局变量
-
将使用中无法避免的全局变量缓存到局部
function getBtn() { let oBtn1 = document.getElementById('btn1') let oBtn3 = document.getElementById('btn3') let oBtn5 = document.getElementById('btn5') let oBtn7 = document.getElementById('btn7') let oBtn9 = document.getElementById('btn9') } //做了缓存 function getBtn2() { let obj = document let oBtn1 = obj.getElementById('btn1') let oBtn3 = obj.getElementById('btn3') let oBtn5 = obj.getElementById('btn5') let oBtn7 = obj.getElementById('btn7') let oBtn9 = obj.getElementById('btn9') }
通过原型新增方法
-
在原型对象上新增实例对象需要的方法
//通过构造函数添加 this var fn1 = function() { this.foo = function() { console.log(11111) } } let f1 = new fn1() //使用原型对象 prototype var fn2 = function() {} fn2.prototype.foo = function() { console.log(11111) } let f2 = new fn2()
避开闭包陷阱
- 闭包特点:
- 外部具有指向内部的引用
- 在”外“部作用域访问”内“部作用域的数据
-
闭包使用不当很容易出现内存泄漏
function foo(){ var el = document.getElementById('btn') el.onclick = function() { console.log(el.id) } el = null //解决,代码+DOM引用都消失了 } foo() //内存会泄漏
避免属性访问方法使用
-
JS不需要属性的访问方法,所有属性都是外部可见的
-
使用属性访问方法只会增加一层重定义,没有访问的控制力
//访问方法 function Person() { this.name = 'icoder' this.age = 18 this.getAge = function() { return this.age } } const p1 = new Person() const a = p1.getAge() //直接访问成员 function Person() { this.name = 'icoder' this.age = 18 } const p2 = new Person() const b = p2.age
For循环优化
-
提前获取遍历长度
for (var i = arrList.length; i; i--) { console.log(arrList[i]) }
采用最优循环方式
//forEach < for < forin
var arrList = new Array(1, 2, 3, 4, 5)
arrList.forEach(function(item) {
console.log(item)
})
for (var i = arrList.length; i; i--) {
console.log(arrList[i])
}
for (var i in arrList) {
console.log(arrList[i])
}
节点添加优化
-
节点的添加操作必然会有回流和重绘
-
文档碎片添加节点
const fragEle = document.createDocumentFragment() for (var i = 0; i < 10; i++) { var oP = document.createElement('p') oP.innerHTML = i fragEle.appendChild(oP) } document.body.appendChild(fragEle)
克隆优化节点操作
var oldP = document.getElementById('box1')
for (var i = 0; i < 3; i++) {
var newP = oldP.cloneNode(false)
newP.innerHTML = i
document.body.appendChild(newP)
}
直接量替换Object操作
//直接量快
var a = [1, 2, 3]
//Object操作
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
本文首发于我的GitHub博客,其它博客同步更新。