JavaScript性能优化---------垃圾回收

JavaScript性能优化---------垃圾回收

我们都知道,随着软件行业的不断发展,性能优化是一个不可避免的话题,那么什么的行为才能算得上是性能优化呢,本质上来说任何一种可以提高运行效率,减少运行开销的行为都可以看作是优化操作。这就意味着软件开发中必然存在着很多值得优化的地方。

在前端开发的过程中,性能优化也更是无处不在,例如请求资源时用到的网络,以及数据的传输方式,开发框架等,都可以去进行优化。

接下来学习JavaScript本身的优化,具体来说就是从认识内存空间的使用,到垃圾回收方式的介绍,从而编写出高效的JavaScript代码。

我们会遇到的内容:

  1. 内存管理(为什么内存需要管理及基本流程);
  2. 垃圾回收机制与GC算法;
  3. V8引擎的垃圾回收;
  4. Performence工具 对内存进行监控,发现代码中是否存在可以优化的性能空间。
  5. 代码优化实例

内存管理

随着计算机硬件技术的不断发展,同时高级编程语言也都自带了GC机制。所以让我们在不需要注意内存空间使用的情况下,也能够完成相应的功能开发。

那为什么还需要重提内存管理呢?

fn函数中有一个空数组,在赋值的时候刻意选择了一个比较大的数字作为下标,目的时为了当前函数调用的时候可以向内存尽可能的去申请一片比较大的空间。

function fn(){
    let arr = [];
    arr[100000] = 999
};
fn()

上面代码语法上是不存在问题的,但是当我们用性能监测工具在脚本执行过程中进行监控的时候,会发现它的内存变化是这样的:如图中的蓝色线条一样,持续升高,过程中并没有回落,这就是内存泄漏。
在这里插入图片描述
所以说我们在写代码的时候对内存管理的机制不够了解的话,就容易写出一些不容易查询到的造成内存泄漏的代码。这些代码多了以后就会给程序造成意想不到的问题。所以掌握内存管理是非常有必要的。

内存管理介绍:
  1. 内存:有可读性单元组成,表示一片可操作的空间。
  2. 管理:人为的去操作一片内存空间的申请、使用和释放。
  3. 内存管理:开发者主动向内存 申请空间、使用空间、释放空间。
  4. 管理流程:申请 - 使用 - 释放
JavaScript中的内存管理

和其他语言一样,JavaScript也是分三步来执行这个过程:申请空间、使用空间、释放空间。

但是ECMAScript中并没有提供相应的API,所以JavaScript开发者不能主动调用相应的API来完成相应的内存管理。

但我们依然可以通过JS脚本来演示当前在内部一个空间的生命周期是如何完成的:

//申请----JavaScript执行引擎在遇到变量定义语句的时候会自动分配给我们相应的空间
let obj = {};
//使用----读写操作
obj.name = 'wjp';
//释放----JavaScript也没有相应的释放API,但可以通过间接的方式,比如把变量设置为null
obj = null;

然后可以在性能监测工具当中看一下这样的走势。

JavaScript当中的垃圾回收

垃圾
  • JavaScript中的内存管理是自动的 (每当我们去创建一个对象,数组或者函数的时候JavaScript会自动分配相应的内存空间)
  • 对象不再被 引用 时就是垃圾 (后续代码在执行过程当中如果通过一些引用关系无法去找到某些对象的时候,这些对象就会被看作是垃圾)
  • 对象不能 从根上访问 到时也是垃圾 (再或者说这些对象已经存在,但由于一些不合适的语法或者结构性的错误让我们没有办法去找到这样一个对象,这种对象也会被看作垃圾)

知道了什么是垃圾之后,JavaScript执行引擎就会出来工作,把它们所占据的内存空间进行回收。这个过程就是我们所说的JavaScript垃圾回收

可达对象
  • 可以访问到的对象就是可达对象 (具体的引用、当前上下文当中通过作用域链能够找到)
  • 可达的标准就是从根出发能够找得到
  • JavaScript中的根可以理解为全局变量对象(全局执行上下文)
总结

所以JavaScript当中的垃圾回收就是:找到垃圾,然后通过JavaScript执行引擎来进行空间的释放回收。

JavaScript中的引用与可达
  • 引用
//当前对象空间叫做 '小明空间',并且小明空间被obj对象所引用。
//且在全局执行上下文中,当前obj对象可以从根上被找到,所以obj是可达的。所以小明对象空间也是可达的。
let obj = {name:'小明'};
//让ali变量等于obj变量,意味着小明空间又多了一次引用,存在引用数值变化。
let ali = obj;

//obj 到小明空间的引用被断掉了,那小明对象空间是否还是可达呢?
//必然是可达鸭,因为ali还在引用着小明对象空间
obj = null;
  • 可达操作
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的对象空间;

obj1和obj2又通过next和prev属性互相引用,所以代码里的对象都可以从根上来进行查找,如图:
在这里插入图片描述

那现在,做一件事情,通过delete语句把obj中o1的引用和obj2对obj1的应用都delete掉:

delete obj.o1;
delete obj.o2.prev;

此时此刻就再也没有办法去找到obj1这个对象空间了。它就变成了垃圾,JavaScript执行引擎就会找到它进行回收。

如图:
在这里插入图片描述

当前在编写代码时会存在对象引用的关系,然后可以从根的下边进行查找,按照这样一些链条终归能找到一些对象,但如果找到这些对象的路径被破坏掉,我们就没有办法找到它,就会把它视作垃圾,最后垃圾回收机制会把它回收掉。

GC算法

GC就是垃圾回收机制的简写,它可以找到内存中的垃圾对象,并释放,回收空间。

垃圾:

- 程序中不再需要使用的对象;(使用完成后上下文不会在用到它)

  • 程序中不能再访问到的对象;(运行过程中变量能否被引用到)
GC算法是什么
  1. GC是一种机制,它里面的垃圾回收器去完成垃圾回收的具体工作;
  2. 工作内容就是查找垃圾,释放空间,回收空间;
  3. GC算法就是GC机制工作时查找和回收后如何分配所遵循的规则;
常见的GC算法
  1. 引用计数
  2. 标记清除
  3. 标记整理
  4. 分代回收

引用计数算法

**核心思想:**设置引用计数器,维护当前对象的引用数值,通过数值是否为零。从而判断是不是垃圾对象。当数值为零时,GC就开始工作,将其所在的对象空间进行回收释放再使用。

当某个对象空间的引用关系发生改变时,引用计数器就会自动去修改当前对象所对应的引用数值。

什么叫引用关系发生改变呢?比如代码里现在有一个对象空间,有一个变量名指向它,那么这个时候数值加1,又多一个变量还指向它,那么数值再加1,如果减少,减1。当引用数值为零时,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 = 1;
    const num2 = 2;
}
fn();

上面代码,从user1、user2、user3、nameList的引用计数肯定都不是零。

fn执行后,由于num1和num2只能在函数作用域内被访问到,从外部全局作用域出发就不能够再找到num1和num2了,这个时候num1和num2身上的引用计数就会回到零,GC立即开始工作,将它们当作垃圾进行回收。

总结一下:靠着当前对象身上的引用计数数值来判断是否为零,从而决定是否是垃圾对象。

**优点:**发现垃圾立即回收,最大限度减少程序暂停

缺点:

  • 无法回收循环利用的对象:因为这样的情况意味着当前对象空间的引用计数器的数值永远是不为零的,不能触发垃圾回收操作
  • **时间开销大:**因为当前的引用计数器需要去维护数值的变化,它要时刻监控当前对象的引用数值是否需要修改。数值嗯到修改需要时间,如果说内存里有更多的对象需要修改,那么相较其他的GC算法,引用计数算法的时间会更大一些
循环利用的对象:
function fn(){
    const obj1 = {};
    const obj2 = {};
    obj1.name = obj2;
    obj2.name = obj1;
}
fn();
//fn函数执行完后,它内部所在的空间要涉及到空间回收的情况,比如obj1和obj2,因为全局作用域已经不可能访问到它了,但这时候的问题是,当GC去回收obj1的时候,发现obj2的属性指向obj1。

上面代码,按照之前的规则,我们在全局作用域找不到obj1和obj2了,但是由于obj1和obj2两者之间在函数作用域内明显还存在互相的指引关系,所以它们当前引用计数器的数值永远是不为零的,这个时候引用计数算法下的GC就没有办法将这两个对象空间进行回收了。从而造成内存空间的浪费,这就是所谓的对象之间的循环引用。这也是计数算法所面临的问题。

标记清除算法

核心思想:标记清除两个阶段

  • 遍历所有对象,找到活动对象并且进行标记(活动对象和之前的可达对象是一个意思)
  • 遍历所有对象,清除没有标记的对象,且会把第一阶段设置的标记抹除,便于GC下次正常工作。
  • 通过两次遍历行为,把当前的垃圾空间进行回收,最终交给相应空闲列表进行维护,方便后续程序申请使用。

如下图:

在全局作用域,可以找到A、B、C三个可达对象并标记 ,找到这三个可达对象后发现它们的下边会有一些子引用,标记清除算法会用递归的方式继续去寻找那些可达的对象,D和E也会被找到并标记;

a1和b1放在右侧,可以比作放在一个局部作用域,而局部作用域执行完毕后就会进行空间回收。所以global链条下是找不到a1和b1的,GC机制就会认为它是垃圾对象,不会给它做标记,然后GC工作时就会找到a1和b1把它们回收。
在这里插入图片描述
简单再整理一下,就是 分成两个步骤:

  1. 第一个步找到所有可达对象,如果涉及到了对象引用的层次关系,那么它会递归的进行查找。然后把找到的可达对象进行标记。
  2. 第二步找到没有被标记的对象,清除,同时会把第一阶段设置的标记抹除。
  3. 同时还会把回收的空间放在当前一个叫空闲列表中。方便后续程序可以直接申请使用。

相比引用计数算法,标记清除算法的原理实现更加简单,还能解决一些相应的问题,在后续的V8当中,会被大量使用到。

优点:

**相对于引用计数算法,它可以解决之前对象循环引用的回收操作。**上图的a1和b1在在局部作用域完成后就失去了和全局作用域global的连接,它们是不可达的对象 ,不可达对象在标记阶段就不能完成标记。第二个阶段找到它们是没有标记的对象,然后清除它们,完成释放。

缺点:
  1. 不会立即回收垃圾对象;(即使在遍历的过程中,它发现了不可达对象也会在第二步进行清除,而且它清除时程序是停止工作的。)
  2. 会产生空间碎片化,**不能让空间得到最大化的使用。**当前所回收的垃圾对象在地址上是不连续的。由于不连续导致回收之后它们分散在各个角落,后续想要使用的时候新的 申请空间大小刚好和它们匹配就能直接用,一旦是多了或者少了就不太适合用。
    在这里插入图片描述

标记整理算法

和标记清除算法一样,这个算法在V8当中也会被频繁使用。

实现原理:

  1. 标记清除的增强。因为它们在第一个阶段的标记操作是完全一样的。都会去遍历所有对象,然后将当前可达活动对象进行标记。
  2. 在清除阶段,标记清除算法是将没有标记的对象做空间回收
  3. 但是标记整理算法会在清除前执行整理地址空间的操作,移动对象的位置,让它们在地址上产生连续

回收前内存对象摆放位置:包含活动对象、非活动对象、空闲的空间 ,执行标记阶段时会将活动对象进行标记,然后进行整理的操作;
在这里插入图片描述

整理后看到就是位置上的改变,它会将活动对象进行移动,在地址上变成一个连续的位置。然后将活动对象右侧的范围进行整体回收;
在这里插入图片描述

回收后,相对于标记清除算法来说,好处就是在内存里面就不会出现大批量的分散小空间。而回收到的空间基本上是连续的。在后续使用过程中去申请新的空间时候就会最大化利用当前所释放出来的空间。在这里插入图片描述
这就是标记整理算法,它会配合标记清除算法在V8引擎中实现频繁的GC操作。

初识V8

V8是目前最主流的JavaScript 执行引擎,例如crome浏览器、nodeJs平台。JavaScript 之所以能在它们上面高效运转,也正是因为V8个幕后英雄的存在。

V8采用即使编译,运行速度快

V8内存设有上限 64位操作系统不超过1.5G,32位不超过800M。

V8垃圾回收策略

前置描述:在程序的使用过程中会用到很多数据,这些数据可以分为原始数据和对象类型数据。原始数据是由程序的语言自身来控制的;所以一般所说的回收指的是当前存活在堆区里面的对象数据。因此这个过程离不开内存操作。

而v8当中对内存做了限制,在这种情况下我们就想知道它是如何对垃圾进行回收的。

  • 分代回收的思想
  • 把当前的内存空间分为两类:新生代、老生代
  • 针对不同代的对象采用不同的最高效的GC算法,从而实现V8高效的垃圾回收。

在这里插入图片描述

这就意味着V8会使用更多的GC算法:

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 增量标记 (提高效率)
V8如何回收新生代对象

V8内部的内存分配:

基于分代的垃圾回收思想,它把内存空间分为两个部分:左侧的from–to区域、右侧的老生代区域

在这里插入图片描述

V8内存空间一分为二;小空间用来存储新生代对象 (32M|16M);

新生代对象指的是存活时间较短的对象,例如局部作用域内的变量在执行完毕后就要被回收,全局变量则会等到程序退出之后才会被回收;所以相对来说新生代对象指的就是存活时间较短的对象。

新生代对象回收实现

  • 回收过程采用复制算法标记整理算法
  • 新生代内存区也会分为两个相等大小的空间:from空间 和 to空间
  • 使用空间称之为from状态空闲空间称为为to状态
  • 代码运行 申请空间 时首先会将变量对象存放于from空间,to空间则空闲着没有被使用。
  • from空间应用到一定程度触发GC机制,使用标记整理算法在from空间进行活动对象的标记,标记过后使用整理的操作将它们的位置变得连续,便于后续不会产生碎片化的空间。
  • 之后将活动对象拷贝至to空间,此时意味着from空间的活动对象在to空间有了备份,就可以开始回收操作了;
  • 将from空间完全释放,然后 from 与 to 交换空间。

回收细节说明

拷贝过程可能会出现晋升:晋升就是将新生代对象移动至老年代进行存储。

  1. 新生代对象里有变量对象也会在老生代对象里面出现
  2. 一轮GC过后还存活的新生代对象
  3. 拷贝的过程中发现to空间的使用率超过25%,否则将来它变成使用空间时可能不够用了
V8如何回收新生代对象

老生代对象存放于老生代存储区域,64位操作系统1.4G,32位操作系统700M。

老生代对象指存活时间较长的对象,例如全局作用域下存放的变量、闭包所放至的数据。

老生代对象回收实现

采用标记清除、标记整理、增量标记算法;

  • 主要采用标记清除算法完成垃圾空间的回收;虽然会存在空间碎片化的问题,但提升的速度非常明显;
  • 采用标记整理进行空间优化;如果发现新生代区域内容往老生代区域移动,老生代存储区不足以存放移过来的对象(晋升)时触发标记整理
  • 采用增量标记进行效率优化

增量标记如何优化垃圾回收

图中分出来两个部分:程序执行和垃圾回收。

需要明确的是:当进行垃圾回收操作时,会阻塞JavaScript程序正常执行。所以图中程序执行会出现空档期。
在这里插入图片描述

所谓的标记增量,就是将整个的垃圾回收操作分为多个小段,组合去完成整个回收,从而替代一口气做完的垃圾回收操作。

这里针对的是老年代区域,所以存在遍历操作。在遍历的过程需要做标记,但它可以不一口气做完,因为它存在直接可达和间接可达对象,一层一层的标记操作可以分割进行。

好处就是 让垃圾回收和程序执行交替完成;时间消耗更合理一些。

整个V8最大的垃圾回收,当达到1.5G的时候采用非增量标记的垃圾回收时间也不到1秒,所以这个时间间隔是合理的。

细节对比
  1. 新生代区域垃圾回收 空间换时间 (采用复制算法,总会有空闲空间存在,但这一部分空间相对于时间上的提升是值得的)
  2. 老生代垃圾回收不适合复制算法,因为它的空间很大,分出一半就是几百M,太奢侈了;而且老生代区域存放的数据很多,采用复制算法也会很消耗时间。
V8垃圾回收总结
  1. V8是一款主流的JavaScript执行引擎;
  2. V8内存设置上限;(这个大小足够web应用使用)
  3. 分代回收思想,新生代和老生代;
  4. V8垃圾回收常见GC算法;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值