JavaScript内存管理
内存管理介绍
- 内存:由读写单元组成,表示一片可操作的空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请 – 使用 – 释放
JavaScript的垃圾回收
JavaScript的垃圾
- JavaScript 中内存管理是自动的
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
JavaScript 中的可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript中的根就可以理解为是全局变量对象
GC算法介绍
GC 定义与作用
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间
GC 里的垃圾是什么
- 程序中不再需要使用的对象
- 程序中不能再访问到的对象
GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾释放空间、回收空间
- 算法就是工作时查找和回收所遵循的规则
常见GC算法
- 引用计数
- 标记清除
- 标记整理
- 分代回收
引用计数算法实现原理
引用计数算法
- 核心思想:设置引用数,判断当前引用数是否为 0
- 引用计数器
- 引用关系发生改变时修改引用数字
- 引用数字为 0 时立即回收
引用计数算法优缺点
引用计数算法优点
- 发现垃圾时立即回收
- 最大限度减少程序暂停
引用计数算法缺点
- 无法回收循环引用的对象
- 时间开销大
标记清除算法实现原理
标记清除算法
- 核心思想:分标记和清除二个阶段完成
- 遍历所有对象找标记活动对象
- 遍历所有对象清除没有标记对象
- 回收相应的空间
标记清除算法优缺点
标记清除算法优点
- 可以回收循环引用的对象
标记清除算法缺点
- 容易产生碎片化空间,浪费空间
- 不会立即回收垃圾对象
标记整理算法实现原理
标记整理算法原理
- 标记整理可以看做是标记清除的增强
- 标记阶段的操作和标记清除一致
- 清除阶段会先执行整理,移动对象位置
标记整理算法优缺点
标记整理算法优点
- 减少碎片化空间
标记整理算法缺点
- 不会立即回收垃圾对象
认识V8
认识V8
- V8 是一款主流的JavaScript执行引擎
- V8 采用即时编译
- V8 内存设限 (64位操作系统一般不超过1.5G,32位不超过800M)
V8 垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8 中常用的 GC 算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
V8 如何回收新生代对象
V8 内存分配
- V8 内存空间一分为二
- 小空间用于存储新生代对象 (32M | 16M)
- 新生代指的是存活时间较短的对象
新生代对象回收实现
- 回收过程采用复制算法 + 标记整理
- 新生代内存区分为二个等大小空间
- 使用空间为 From,空闲空间为 To
- 活动对象存储于 From 空间
- 标记整理后将活动对象拷贝至 To
- From 与 To 交换空间完成释放
回收细节说明
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 一轮 GC 还存活的新生代需要晋升
- To 空间的使用率超过 25%
V8 如何回收老生代对象
老生代对象说明
- 老生代对象存放在右侧老生代区域
- 64 位操作系统 1.4G,32位操作系统 700M
- 老生代对象就是指存活时间较长的对象
老生代对象回收实现
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化
- 采用增量标记进行效率优化
细节对比
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法
Performance工具介绍
为什么使用 Performance
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance 提供多种监控方式
Performance 使用步骤
- 打开浏览器输入目标网址
- 进入开发人员工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的外在表现
- 页面出现延迟加载或经常性暂停
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
监控内存的几种方式
界定内存问题的标准
- 内存泄露:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图来分析
监控内存的几种方式
- 浏览器任务管理器(shift + Esc)
- Timeline 时序图记录 (开发者工具 - 性能 - 内存)
- 堆快照查找分离 DOM (开发者工具 - 内存 - 堆快照)
- 判断是否存在频繁的垃圾回收
为什么确定频繁垃圾回收
- GC工作时应用程序是停止的
- 频繁且过长的 GC 会导致应用假死
- 用户使用中感知应用卡顿
确定频繁的垃圾回收
- Timeline 中频繁的上升下降
- 任务管理器中数据频繁的增加减少
代码优化介绍
如何精准测试 JavaScript 性能
- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于 Benchmark.js 的 https://jsperf.com 完成
Jsperf 使用流程
- 使用 GitHub 账号登录
- 填写个人信息 (非必须)
- 填写详细的测试用例信息 (title、slug)
- 填写准备代码 (DOM 操作时经常使用)
- 填写必要有 setup 与 teardown 代码
- 填写测试代码片段
慎用全局变量
为什么要慎用
- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出
- 如果某个局部作用域出现了同名变量则会遮蔽或污染全局
缓存全局变量
- 将使用中无法避免的全局变量缓存到局部
通过原型新增方法
- 在原型对象上新增实例对象需要的方法
避开闭包陷阱
闭包特点
- 外部具有指向内部的引用
- 在 “外” 部作用域访问 “内” 部作用域的数据
关于闭包
- 闭包是一种强大的语法
- 闭包使用不当很容易出现内存泄露
- 不要为了闭包而闭包
避免属性访问方法使用
JavaScript 中的面向对象
- 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 循环优化
var btns = document.getElementByClassName('btn')
for(var i = 0; i<btns.length; i++){
console.log(i)
}
for(var i = 0, len = btns.length; i<len; i++){
console.log(i)
}
采用最优循环方式
var arr = [1,2,34,30,6]
arr.forEach(function(item){
console.log(item)
})
for(var i = arr.length; i; i--){
console.log(arr[i])
}
for(var i in arr){
console.log(arr[i])
}
节点添加优化
- 节点的添加操作必然会有回流和重绘
for(var i = 0; i<10; i++){ var op = document.createElement('p') op.innerHTML = i document.body.appendChild(op) } 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)
克隆优化节点操作
for(var i = 0; i<3; i++){
var op = document.createElement('p')
op.innerHTML = i
document.body.appendChild(op)
}
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]
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
JSBench 使用
- 测试代码的速度性能
- 网站地址: jsbench.me,
堆栈中代码执行流程
减少判断层级
function doSomething(part, chapter){
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if(part){
if(parts.includes(part)){
console.log('属于当前课程')
if(chapter > 5){
console.log('您需要提供 VIP 身份')
}
}
}else{
console.log('请确认模块信息')
}
}
doSomething('ES2016', 6)
function doSomething(part, chapter){
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if(!part){
console.log('请确认模块信息')
return
}
if(!parts.includes(part)) return
console.log('属于当前课程')
if(chapter > 5){
console.log('您需要提供 VIP 身份')
}
}
doSomething('ES2016', 6)
减少作用域链查找层级
var name = 'zce'
function foo(){
name = 'zce666' //这里的name是属于全局的
function baz(){
var age = 38
console.log(age)
console.log(name)
}
baz()
}
foo()
var name = 'zce'
function foo(){
var name = 'zce666'
function baz(){
var age = 38
console.log(age)
console.log(name)
}
baz()
}
foo()
减少数据读取次数
var oBox = document.getElementById('skip')
// function hasEle(ele, cls){
// return ele.className == cls
// }
function hasEle(ele, cls){
var clsname = ele.className
return clsname == cls
}
hasEle(oBox, 'skip')
字面量与构造式
- 字面量的声明执行效率大于构造式
let test = () => { let obj = new Object() obj.name = 'zce' obj.age = 18 obj.slogan = '我为前端而活' return obj } let test = () => { let obj = { name: 'zce', age: 18, slogan: '我为前端而活' } return obj } test() var str1 = 'zrc我为前端而活' var str2 = new String('zrc我为前端而活')
减少循环体活动
- 将固定不变的值抽离到循环体外部
- while循环的改造
var test = () => { var i var arr = ['zce', 38, '我为前端而活'] for(i = 0; i<arr.length; i++){ console.log(arr[i]) } } var test = () => { var i var arr = ['zce', 38, '我为前端而活'] var len = arr.length for(i = 0; i<len; i++){ console.log(arr[i]) } } var test = () => { var arr = ['zce', 38, '我为前端而活'] var len = arr.length while(len--){ console.log(arr[len]) } } test()
减少声明及语句数
//减少声明
var oBox = document.getElementById('box')
var test = (ele) => {
let w = ele.offsetWidth
let h = ele.offsetHeight
return w * h
}
var test = (ele) => {
return ele.offsetWidth * ele.offsetHeight
}
console.log(test(oBox))
//减少语句数
var test = () => {
var name = 'zce'
var age = 18
var slogan = '我为前端而活'
return name + age + slogan
}
var test = () => {
var name = 'zce',
age = 18,
slogan = '我为前端而活'
return name + age + slogan
}
console.log(test())
惰性函数与性能
var oBtn = document.getElementById('btn')
function foo (){
console.log(this)
}
// function addEvent(obj, type, fn){
// if(obj.addEventListener){
// obj.addEventListener(type, fn, false)
// }else if(obj.attachEvent){
// obj.attachEvent('on' + type, fn)
// }else{
// obj['on' + type] = fn
// }
// }
function addEvent(obj, type, fn){
if(obj.addEventListener){
addEvent = obj.addEventListener(type, fn, false)
}else if(obj.attachEvent){
addEvent = obj.attachEvent('on' + type, fn)
}else{
addEvent = obj['on' + type] = fn
}
return addEvent
}
addEvent(oBtn, 'click', foo)
采用事件委托
var list = document.querySelector('li')
// function showTxt(ev){
// console.log(ev.target.innerHTML)
// }
// for(let item of list){
// item.onclick = showTxt
// }
var oUl = document.getElementById('ul')
oUl.addEventListener('click', showTxt, true)
function showTxt(ev){
var obj = ev.target
if(obj.nodeName.toLowerCase() === 'li'){
console.log(obj.innerHTML)
}
}