Java教程

JavaScript性能优化

本文主要是介绍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操作。

这篇关于JavaScript性能优化的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!