JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理V8分代回收Performance使用)
Posted JIZQAQ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理V8分代回收Performance使用)相关的知识,希望对你有一定的参考价值。
目录
一、导入
随着我们javascript代码需要实现的功能越来越复杂,性能优化变得重要了起来。那么哪些内容可以被看做是性能优化呢?本质上来说,任何一种提高运行效率,降低运行开销的行为都可以看做是优化操作。
前端优化无处不在,例如请求资源时候用到的网络、数据的传输方式、开发过程中使用的框架等。本阶段讨论的核心是JavaScript语言本身的优化,也就是从认知内存空间的实用到垃圾回收的方式介绍。从而让我们编写出高效的JavaScript代码。
在这篇文章里,主要讨论内存管理相关的内容。
随着这些年来的硬件不断发展,同时高级编程语言当中都自带GC机制,这些变化都让我们可以在不需要过多注意内存空间的使用下,也能正常完成相应的功能开发。
二、内存管理
- 内存:由可读写单元组成,表示一篇可操控空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
1.JavaScript中的内存管理
分为下面三个步骤
- 申请内存空间
- 使用内存空间
- 释放内存空间
//DEMO2
// Memory management
// 申请
let obj =
// 使用
obj.name = 'lg'
// 释放 js中没有释放api,我们这里就把它设置为null
obj = null
2.JavaScript中的垃圾回收
概念
对我们前端而言JS的内存管理是自动的,每次我们创建一个对象、数组、函数的时候会自动分配一个内存空间。
那么什么是垃圾呢?
- 对象不再被引用的时候就是垃圾
- 对象不能从根上访问到的时候也是垃圾。
知道什么是垃圾之后,JS引擎就会出来工作,把它们占据的对象空间进行回收,这就叫做JS的垃圾回收。
下面我们引入一个概念,叫可达对象
可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript中的根就可以理解为是全局变量对象
接下来我们在代码中看一下JS的引用和可达是什么样的
引用样例
// reference
let obj = name: 'xm'// 这里就发生了引用,obj也是可达的,xm空间也是可达的
let ali = obj // xm空间又多了一次引用
obj = null // obj引用xm的这条路断了,但是xm空间还是可达的,因为ali还在引用xm空间
console.log(ali)//打印 name: 'xm'
可达样例
先看一下下面这段代码
//DEMO4
//可达对象
//我们定义了一个函数去接收两个变量obj1,obj2,互相指引
function objGroup(obj1,obj2)
obj1.next = obj2
obj2.prev = obj1
return
o1:obj1,
o2:obj2
let obj = objGroup(name:'obj1',name:'obj2')
console.log(obj)
说明:
首先从全局的根出发,我们能找到一个可达的对象obj,它是通过一个函数调用之后,指向了一个内存空间(里面就是o1和o2),又通过相应的属性指向obj1和obj2的空间,这两个空间之间通过next和prev互相指向。
所以在这个例子里面,我们能从根访问到任何一个内存空间。
然后我们删除掉两行代码之后
function objGroup(obj1,obj2)
obj1.next = obj2
//obj2.prev = obj1
return
//o1:obj1,
o2:obj2
let obj = objGroup(name:'obj1',name:'obj2')
console.log(obj)
这之后,我们所有能够找到obj1的线条都被删除了,于是obj1空间就会被认为是垃圾,js引擎就会找到它把它删除。
3.GC算法介绍
GC定义与作用
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间。
GC里的垃圾是什么
- 程序中不再需要使用的对象(下面例子里的name)
- 程序中不能再访问到的对象(下面例子里的name)
GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾释放空间、回收空间
- 算法就是工作时查找和挥手所遵循的规则
常见GC算法
- 引用计数:通过一个数字判断当前的是不是垃圾
- 标记清除:进行工作的时候给到活动对象添加一个标记,判断是否垃圾
- 标记整理:和标记清除类似,回收过程中做的事情不太一样
- 分代回收:不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
3.1引用计数
实现原理
核心思想:设置应用数,判断当前引用数是否为0,从而判断是否垃圾对象。数字为0,GC开始工作,将其所在的对象空间回收再释放使用。
引用关系发生改变的时候,引用计数器会主动去修改引用数值。引用数字为0的时候立刻回收。
//DEMO4
//GC 引用计数
const user1 = age:11
const user2 = age:22
const user3 = age:33
const nameList = [user1.age,user2.age,user3.age]
function fn()
// 当加上const之后,num1作用域变了,一旦fn()结束之后,我们就再也找不到num1了,引用计数为0,gc就会吧num1的内存空间回收
const num1 = 1
num2 = 2
fn()
总结:
靠着我们当前对象身上引用计数器的数值是否为0,从而决定它是不是垃圾对象。
引用计数的优点
- 发现垃圾时立即回收
- 最大限度减少程序暂停(当内存快满的时候就立刻去找引用计数为0的删掉)
引用计数的缺点
- 无法回收循环引用的对象
- 时间开销大(因为要时刻监控数值的修改)
什么是循环引用的对象
//DEMO5
//对象之间的循环引用
//虽然obj1和obj2全局作用域下找不到了,但是引用还存在的,obj1和obj2互相在他们的作用域内引用
//用引用计数算法无法释放这部分空间。
function fn()
const obj1 =
const obj2 =
obj1.name = obj2
obj2.name = obj1
return 'hello world'
fn()
3.2标记清除
实现原理
这个原理实现要比引用计数算法更加简单,还能解决一些问题。在后续学习的v8当中会被大量用到。
核心思想:将整个垃圾回收操作分成标记和清楚两个阶段完成。第一个阶段会遍历所有对象,找到活动对象标记。第二个阶段仍然遍历所有对象,把那些身上没有标记的对象进行清除。还会把第一个阶段的标记抹掉,便于GC下次正常工作。通过两次遍历行为,把我们当前的垃圾空间进行回收,最终交给相应的空闲列表去维护。
通过下面图片举例说明一下:
在全局的地方,我们可以找到A、B、C三个可达对象,如果发现他们的下边有child,或者child还有child,它会使用递归的方式去寻找可达对象。所以D、E也会被做上可达标记。从global的链条下是找不到a1,b1的,所以GC工作的时候会认为a1、b1是垃圾,把它清除掉,并且把可达标记都清除掉。要注意最终还会把回收的空间放在一个空闲列表上面,方便后面的程序直接申请空间使用。
标记清除优点
可以解决对象循环引用的回收操作
标记清除缺点
如图显示,左右两侧有从根无法被查找的区域,这种情况下在第二轮进行清除操作的时候,就会直接吧两侧对应的空间回收,然后把释放的空间添加到空闲链表之上。紧接着后续的程序进来,在空闲链表上申请空间。
但是我们从图片上看得到,即时左右两边的空间被释放,但是他们这两块空间被中间的内容给分割着,空出来的内存地址不连续,是分散的。假如如图的例子,左侧空出2个空间,右侧空出1个空间,而我们想要申请一个1.5的空间。我们申请左侧呢,就会空余0.5个空间,而右边空间又不足。
这就是标记清除最大的问题,会造成空间碎片化,不能让我们空间最大化使用。
标记清除不会立即回收垃圾对象。
3.3标记整理
实现原理
标记整理可以看作是标记清除的增强操作,他在标记阶段的操作和标记清除一致,但是清楚阶段会先执行整理,移动对象位置。
下面用图片来更好的 解释一下标记整理回收阶段的过程。
回收之前我们内存摆放位置如下,有着很多活动对象、非活动对象和空闲的空间,当他去执行当前标记操作的时候,会把所有的活动对象进行标记,紧接着会去进行一个整理的操作。
整理会把我们的活动对象进行移动,在地址上变成连续的位置,紧接着将当前活动对象右侧的范围进行回收。
回收之后大概长下面的样子,现在回收到的空间基本上都是连续的,后续可以最大化利用内存释放出来的空间。它会配合着标记清除,在我们的V8引擎当中配合实现频繁的GC操作。
标记整理优点
减少碎片化空间
标记整理缺点
不会立即回收垃圾对象
4.V8
认识V8
V8是一款主流的JavaScript执行引擎,日常我们使用的chrome浏览器和node.js用的都是V8。
V8采用即时编译,将源码直接翻译成机器码,速度非常快。
V8的内存是有设限制的,64位系统上限1.5G,32位系统的上限是800MB。这对网页应用是足够了,官方做过测试,如果垃圾内存达到1.5G的时候,V8采用标记增量算法进行回收只需要消耗50ms,如果采用非标记增量的算法去回收则需要1s。从用户体验来说,1秒已经是很长的时间了,所以以1.5G为界。
V8垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
V8内存分配
- V8内存空间一分为二
- 小的空间用于存储新生代对象(64位是32M,32位是16M)
- 新生代指的是存活时间较短的对象
- 老年代对象存放在右侧老生代区域(64位系统1.4G,32位 700M)
- 老年代对象就是指存活时间较长的对象
V8新生代对象的回收
- 回收过程采用复制算法+标记整理
- 新生代内存分为两个等大小空间
- 使用空间为From,空闲空间为To
- 活动对象存储与From空间
- 标记整理后将活动对象拷贝至To空间
- From与To交换空间完成释放
回收细节说明
- 拷贝过程可能出现晋升
- 晋升就是讲新生代对象移动到老生代
- 一轮GC还存活的新生代需要晋升
- To空间的使用率超过25%的话,也需要晋升
V8老生代对象的回收
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化(当晋升的时候发现,老生代没有足够的空间存放新生代对象的时候,就会进行标记整理的操作)
- 采用增量标记进行效率优化
新老细节对比
- 新生代区域垃圾回收使用空间换时间
- 老年代区域垃圾回收不适合复制算法,空间比较大,存放的数据多,消耗的时间会比较多
标记增量算法
看下面这张图,上半部分是程序的执行,下半部分是垃圾的回收。当垃圾回收进行工作其实是会阻塞JS的执行的。
标记增量的方法是把我们垃圾回收的操作,拆分成多个小步,组合去完成整个回收,从而去替代以前一口气完成的垃圾回收。这样可以程序执行和垃圾回收交替执行,时间消耗更加合理一些。
(中间的标记可能是标记可达元素的子元素之类的。)
把以前很长的停顿时间拆成小段,用户体验更好一些。
5.Performance工具介绍
为什么使用Performance
- GC目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance提供多种监控方式
总的来说,就是通过Performance时刻监控内存啦。
使用步骤
- 打开浏览器输入目标网址(Chrome浏览器)
- 进入开发人员工具面板(F12),选择Performance
- 开启路子功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
出来报告之后记得选择Menory,默认是不选择的。勾选之后会出现下面蓝色线的部分,那块就是内存使用情况了。
6.内存问题外在表现
都是在网络状况正常的情况下
- 页面出现延迟加载或经常性暂停
- 页面持续性出现糟糕的性能
- 页面性能随时间越长越来越差(内存泄漏)
7.监控内存的方式
界定内存问题的标准
- 内存泄漏:内存使用持续升高,没有下降的节点
- 内存膨胀:当前应用参训的本身,为了达到最好的应用效果,需要很大的内存。也许是程序的问题、或者当前硬件设备不支持,导致性能体验有问题。如果在多数设备上都存在性能问题的话,就是我们程序的问题了。
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的方式
- 浏览器任务管理器
- Timeline时序图记录
- 堆快照查找分离DOM
- 判断是否存在频繁的垃圾回收
1.任务管理器监控内存
下面是一个DEMO,在界面中放置一个点击事件,点击事件触发后会生成一个非常长的数组。
新建一个html文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务管理器监控内存变化</title>
</head>
<body>
<button id="btn">Add</button>
<script>
const oBtn = document.getElementById('btn')
oBtn.onclick = function()
let arrList = new Array(1000000)
</script>
</body>
</html>
在VSCode里面安装一个叫open in browser的插件
安装之后在html文件里面点击右键,选择Open In Default Browser
然后会在浏览器当中被打开,我们再打开浏览器的任务管理器,chrome的话是右上角...——更多工具——任务管理器
打开之后就是类似下面这样的界面
这个时候是不显示JavaScript内存的,需要在上面标题的那一栏,右键然后选择JavaScript使用的内存,选择之后最后面就会多一栏了。
第一列内存是DOM的内存,最后一列是说可达对象在使用内存的大小。
找到我们页面对应的那一条数据,然后多次点击页面上的按钮,发现JavaScript内存一直增加,而没有减少,说明GC没有释放就有问题了。
2.Timeline记录内存
使用任务管理器监控内存的变化过程中,我们可以发现,我们只能判断是否存在问题,但是没法定位具体和什么有关。我们用Timeline可以更精准的定位问题。
同样新建一个HTML文件,然后在浏览器中打开
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>时间线记录内存变化</title>
</head>
<body>
<button id="btn">Add</button>
<script>
const arrList = []
function text()
for(let i = 0;i<100000;i++)
document.body.appendChild(document.createElement('p'))
arrList.push(new Array(1000000).join('x'))
document.getElementById('btn').addEventListener('click',test)
</script>
</body>
</html>
这次我们调出F12,Performance里面点击开始录制,然后多点几次Add按钮,过一会停止录制。 观察内存使用变化。
3.堆快照
工作原理
找到当前的JS堆,对它进行照片的留存,有了照片之后我们就可以看到里面的所有信息。这更像是针对分离DOM的查找行为。我们页面上看到的很多元素,其实都是DOM节点,它们本该都是存在于一个存活的DOM树上的。
如果一个节点从当前DOM树上脱离,而且在JS代码中再也没被引用,就成为了垃圾。
如果一个DOM节点只是从树上脱离,但是JS代码里还被引用,这种叫分离DOM。他虽然在页面上看不见了,但是还占据着我们的内存,这种情况下就是一种内存泄漏。我们通过堆快照的功能把它们都找出来,然后再去代码里面把他们找出来进行清除。
什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点
- 分离状态的DOM节点
新建一个HTML文件,然后在浏览器中打开
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>堆快照监控内存</title>
</head>
<body>
<button id="btn">Add</button>
<script>
var tmpEle
function fn()
var ul = document.createElement('ul')
for(var i =0;i<10;i++)
var li = document.createElement('li')
ul.appendChild(li)
tmpEle = ul
document.getElementById('btn').addEventListener('click',fn)
</script>
</body>
</html>
打开F12,选择Memory 。在没有点击Add的情况下,先获得一次快照。然后点击Add按钮,再获得一次快照。
这个时候,我们可以在两次快照里面检索detached,第一次快照里面是找不到的,但是我们能够在第二次快照里面找到。
这几个就是我们界面里添加的dom节点,虽然没有在页面上显示,但是还是内存上添加了,这就是分离dom。
回到我们的代码里,把不需要的置null
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>堆快照监控内存</title>
</head>
<body>
<button id="btn">Add</button>
<script>
var tmpEle
function fn()
var ul = document.createElement('ul')
for(var i =0;i<10;i++)
var li = document.createElement('li')
ul.appendChild(li)
tmpEle = ul
//新增的代码
tmpEle = null
document.getElementById('btn').addEventListener('click',fn)
</script>
</body>
</html>
再次点击Add,并获取快照,我们就搜不到detached了。
4.判断是否频繁垃圾回收
为什么需要判断
- GC工作时候应用程序是停止的
- 频繁且国产的GC会导致应用假死
- 用户使用中感知应用卡顿
如何判断
- Timeline中频繁上升下降,中间时间间隔很短
- 任务管理器中数据频繁增加减少
参考资料
1.拉勾网 《大前端训练营》课程
以上是关于JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理V8分代回收Performance使用)的主要内容,如果未能解决你的问题,请参考以下文章