JavaScript性能优化1——内存管理(JS垃圾回收机制、引用计数、标记清除、标记整理、V8分代回收、Performance使用)

目录

一、导入

二、内存管理

1.JavaScript中的内存管理

2.JavaScript中的垃圾回收

概念

可达对象

引用样例

可达样例

3.GC算法介绍

GC定义与作用

GC里的垃圾是什么

GC算法是什么

常见GC算法

         3.1引用计数

         实现原理

         引用计数的优点

         引用计数的缺点

         什么是循环引用的对象

         3.2标记清除

         实现原理

         标记清除优点

         标记清除缺点

         3.3标记整理

         实现原理

         标记整理优点

         标记整理缺点

4.V8

认识V8

V8垃圾回收策略

V8中常用的GC算法

V8内存分配

V8新生代对象的回收

V8老生代对象的回收

标记增量算法

5.Performance工具介绍

为什么使用Performance

使用步骤

6.内存问题外在表现

7.监控内存的方式

界定内存问题的标准

监控内存的方式

1.任务管理器监控内存

2.Timeline记录内存

3.堆快照

4.判断是否频繁垃圾回收


一、导入

随着我们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时刻监控内存啦。

使用步骤

  1. 打开浏览器输入目标网址(Chrome浏览器)
  2. 进入开发人员工具面板(F12),选择Performance
  3. 开启路子功能,访问具体界面
  4. 执行用户行为,一段时间后停止录制
  5. 分析界面中记录的内存信息

出来报告之后记得选择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.拉勾网 《大前端训练营》课程

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值