内存管理
由开发者主动申请、使用、释放内存空间
JavaScript中的内存管理是自动的
- 申请内存空间
- JavaScript中没有提供内存空间申请的API
- 当定义一个变量,变量被声明赋值时,系统自动分配相应的内存空间
- 使用内存空间
- 对变量进行修改赋值等操作
- 释放内存空间
- JavaScript中没有提供内存空间释放的API
- 手动将变量赋值为null,使内存空间不再被变量所引用,内存将在下次垃圾回收过程中被释放
垃圾回收
- 什么是JavaScript中的垃圾?
- 对象不再被引用时,是垃圾
- 对象不能从根上被访问到时,是垃圾
- 可达对象
- 可以访问到的对象,即为可达对象
- 可达的标准是从根出发能否被访问到
- JavaScript中的根可以理解为全局变量对象
GC算法介绍
GC是垃圾回收机制的缩写
GC可以找到内存中的垃圾,并释放和回收内存空间
- GC的垃圾是什么
- 程序不再使用的对象
- 程序不能再访问到的对象
- 常见GC算法
- 引用计数
- 标记清除
- 标记整理
- 分代回收
引用计数算法
核心思想是,设置引用数,判断当前引用数是否为0,为0则进行垃圾回收
引用关系发生改变时,引用计数器修改引用数
- 优点
- 发现垃圾立即回收
- 最大限度减少程序暂停
- 缺点
- 无法回收循环引用的对象(引用计数不为0)
- 资源开销大
标记清除算法
核心思想是,分为标记和清除两个阶段
先遍历所有对象,找到活动对象(可达可访问)进行标记
再遍历所有对象,回收未被标记的对象,并清除已被标记的对象的标记
- 优点
- 可以回收循环引用对象(不可达对象不会被标记)
- 缺点
- 内存空间碎片化(内存地址不连续),浪费空间
标记整理算法
标记整理算法是标记清除算法的增强
标记阶段与标记清除算法相同
清除阶段会先进行整理操作,移动对象位置,使内存地址连续
V8引擎
V8是一款主流JavaScript执行引擎
采用即时编译
内存设有上限
- V8垃圾回收策略
- 采用分代回收思想,将内存分为新生代、老生代,针对不同对象采用不同算法
- V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 增量增量
- V8内存分配
- 内存空间分为两个部分
- 小空间(64位系统32M|32位系统16M)用于存放新生代对象(存活时间较短的对象)
- 大空间(64位系统1.4G|32位系统700M)用于存放老生代对象(存活时间较长的对象,例如全局对象下存放的对象,以及闭包中存放的对象等)
- 新生代对象回收
- 采用空间复制 + 标记整理算法
- 将新生代内存区分为大小相等的两个空间,使用空间为From,空闲空间为To
- 触发GC时,使用标记整理算法对From空间进行标记,并将活动对象拷贝到To空间
- 回收释放From空间,并与To空间进行互换
- 新生代对象从From空间拷贝到To空间过程中可能出现晋升操作(将新生代对象移动至老生代对象存储区域)
- 一轮GC后还存活的新生代对象将晋升
- To空间使用率超过25%时
- 老生代对象回收
- 采用标记清除、标记整理、增量标记算法
- 先使用标记清除完成垃圾空间回收(回收速度快)
- 当出现晋升时,如果空间不足,会使用标记整理算法进行空间优化
- 采用增量标记进行效率优化
- 垃圾回收会阻塞JavaScript程序代码执行
- 将垃圾回收的对象进行分段,使垃圾回收过程与程序代码执行交替进行,避免需要回收的对象较多时(上限1.5G,回收只需1秒)长时间阻塞程序执行
- 新生代回收 vs 老生代回收
- 新生代空间小,使用空间换时间(空间复制算法)
- 老生代空间大,不适合使用空间复制算法
Performance工具介绍
浏览器中查看web程序执行性能的辅助工具,提供多种监控方式
- 使用方式
- 打开浏览器,输入目标网址
- 打开开发人员工具,进入Performance工具选项页面,点击录制
- 访问目标网址,进行用户操作
- 停止录制
- 查看性能监控信息
监控内存的几种方式
- 浏览器任务管理器
- 快捷键Shift + Esc (Chrome) 查看JavaScript内存使用列
- Timeline时序图记录
- Performance工具中录制查看
- 堆快照查找分离DOM
- 什么是分离DOM?
- 脱离DOM树,未被代码引用的DOM为垃圾,会被回收,仍被代码引用的DOM,即为分离DOM
- 使用浏览器开发者选项中的内存工具Memory,获取堆快照Heap Snapshot
- 过滤关键字,查找Detached,结果中的Detached HTML**Element,即为分离DOM
- 什么是分离DOM?
- 判断是否存在频繁垃圾回收
- 任务管理器或Timeline中查看内存是否频繁变化
代码优化
使用基于benchmark.js的jsperf来评估JavaScript代码的性能
-
慎用全局变量
- 全局对象定义在全局上下文,一直存在与全局上下文执行栈,直到程序退出才会释放
- 局部作用域出现同名变量容易出现变量污染
- 使用局部变量的代码,性能要优于使用全局变量的代码
-
缓存全局变量
- 缓存全局变量,再使用缓存的变量进行代码操作,性能要优于直接使用全局对量,但性能差距非常小
-
通过原型对象添加附加方法
- 构造函数中添加方法,每个实例对象中都会创建一个相同的方法,占用内存
- 构造函数原型对象上添加方法,每个实例共享一个方法,性能更好
-
避开闭包陷阱
闭包在外部作用域访问内部作用域的数据,具有指向内部的引用
闭包外层函数调用完成后,内部函数及变量仍被引用,未被释放,容易引起内存泄露
-
闭包使用完成后,手动将引用清空(赋值为null)
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">hello</button> <script> // function foo() { // let btn = document.getElementById('btn') // btn.onclick = function() { // console.log(btn.id) // } // } // foo() function foo() { let btn = document.getElementById('btn') btn.onclick = function() { console.log(this.id) } btn = null } foo() </script> </body> </html>
-
避免属性访问方法使用
- JavaScript的面向对象的属性是直接对外暴露的,并不需要属性的访问方法(类似Java的getXXX、setXXX方法)
- 属性访问方法相当于在构造函数中添加实例方法
- 直接使用属性访问,在性能上,比使用属性访问方法来访问属性更好
-
For循环优化
-
对要遍历的数组的length进行提前获取并缓存
let array = [1, 2, 3, 4, 5] for (let i = 0, len = array.length; i < len; i++) { console.log(i) }
-
-
选择最优的循环方法
在数据量较大的情况下,执行效率排序
while > for > forEach > for...of > map > for...in
- for
- forEach
- 无法break或return中断
- for...of
- for...in
- map
- while
-
文档碎片(document fragment)优化节点添加
添加dom节点必然会产生回流和重绘,使用文档碎片,将多次节点添加统一成一次,可以减少回流reflow和重绘repaint
- 使用
document.createDocumentFragment
创建文档碎片,并将要添加的节点append到文档碎片中,最后append到body
const fragment = document.createDocumentFragment() for (let i = 0; i < 10; i++) { fragment.append(document.createElement('div')) } document.body.append(fragment)
- 使用
-
克隆优化节点添加
- 使用
element.cloneNode(false)
通过克隆的方式来创建节点,比使用document.createEleemnt()
性能好一些,但差距不是特别大
- 使用
直接字面量替换new Object