JavaScript中的性能优化1

文章说明:本文章为拉钩大前端训练营所做笔记和心得,若有不当之处,还望各位指出与教导,谢谢 !

 本文章内容:

  • 内存管理
  • 垃圾回收与常见GC算法
  • V8引擎的垃圾回收
  • performance工具
  • 代码优化实例

一、内存管理

  • 内存:由可读写单元组成,表示一片可操作空间

  • 管理:人为的去操作一片空间的申请、使用和释放

  • 内存管理:开发者主动申请空间、使用空间、释放空间

  • 管理流程:申请-使用-释放

JavaScript中的内存管理

  • 申请内存空间
  • 使用内存空间

  • 释放内存空间

//申请
// 在JavaScript中执行引擎中遇到变量定义时自动分配给我们一个相应的空间,相当于定义一个变量:
let obj = {}

//使用,相当于读取的操作
obj.name = 'lg'

//释放 
obj = null 
//相当于按照内存管理的流程去实现这样一个内存管理

二、JavaScript中的垃圾回收

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

  • 对象不再被引用时是垃圾

  • 对象不能从根上访问到时是垃圾

JavaScript引擎会把以上这些垃圾内存回收

JavaScript中的可达对象:

  • 可以访问到的对象就是可达对象(引用、作用域链)
  • 可达的标准就是从根出发是否能够被找到

  • JavaScript中的根就可以理解为是全局变量对象

let obj = {name:'xm'}

let ali = obj 

obj = null //此时ali 这个对象仍然能够访问到{name:'xm'}这个空间,{name:'xm'}是可达的
function objGroup(obj1,obj2){
    obj1.next = obj2
    obj2.next = obj1

    return {
        o1:obj1,
        o2:obj2
    }
}

let obj = objGroup({name:'obj1'},{name:'obj2'})

console.log(obj)
/*{
    o1: { name: 'obj1', next: { name: 'obj2', next: [Circular] } },
    o2: { name: 'obj2', next: { name: 'obj1', next: [Circular] } } 
  }
  */

下图是上述个对象的可达图示:

上图可以看出个对象都能有路径访问到,若是切断{name:obj1}的访问路径,则该对象变成不可达,就会变成垃圾被回收,如下图:

三、GC算法介绍

1.GC定义与作用

  • GC就是垃圾回收机制的简写
  • GC可以涨到内存中的垃圾并释放和回收空间

Gc里的垃圾是什么?

  • 程序中不再需要使用的对象
function func(){
    name = 'lg'
    return `${name} is a coder`
}

func()//某个数据使用完后上下文不再去用它了,就可以把它当做垃圾来看待,当函数调用完以后,这里已经不再需要name了,根据需求方面考虑被当做垃圾回收
  • 程序中不能再访问到的对象
function func(){
    const name = 'lg'
    return `${name} is a coder`
}

func()//函数调用完后,外部的空间不能再访问到它了,当我们找不到它的时候,它也可以算作是垃圾

2.GC算法

  • GC是一种机制,垃圾回收器完成具体的工作
  • 工作的内容就是查找垃圾释放空间、回收空间
  • 算法就是工作时查找和回收所遵循的规则

常见GC算法:

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

以上后续会讲到。

3.引用计数算法实现原理

  • 核心思想:设置引用计数器,判断当前引用数是否为0(当引用数为0时,GC开始工作,将其所在的对象空间呢进行回收和释放再使用)
  • 引用计数器(相对于其它的GC算法来说也正是由于引用计数器的存在导致了引用计数在执行效率上可能与其它的计数算法有所差别)
  • 引用关系改变时修改引用数字(当对象引用关系发生改变时,引用计数器就会主动去修改当前对象所对应的引用数值,当我们代码里面现在有一个对象空间,目前有一个变量指向他,那么这个时候我们就把数值加一,如果说这个对象还有其它变量指向它,那么就再加一,如果减少的情况下,就减一,如果计数为0的时候,GC就会立即回收)
  • 引用数字为0时立即回收
// reference count

const user1 = {age:11}
const user2 = {age:22}
const user3 = {age:33}

const nameList = [user1.age,user2.age,user3.age]//user1到3还被这个数组引用着,不会被回收

function fn(){
    const num1 = 1
    const num2 = 2
}

fn()//当函数调用完毕后,num1和num2无法被外部使用,引用计数为0会被回收

4、引用计数算法的优缺点

优点:

  • 发现垃圾时立即回收
  • 最大限度减少程序暂停:应用程序在执行过程中必然会对内存进行消耗而当前的执行平台的内存肯定是有上限的,所以内存肯定有占满的时候,不够由于引用计数算法是时刻监控着那些引用数字为0的对象,所以我们认为:举一个极端的现象就是当发现内存即将要爆满的时候,那么引用计数器就立马去找那些数值为0的对象空间,然后对其进行释放,这样就保证内存不会有被沾满的时候

缺点:

  • 无法回收循环引用的对象
function fn(){
    const obj1 = {}
    const obj2 = {}

    obj1.name = obj2
    obj2.name = obj1

    return 'lg is a coder'
}

fn()

在执行结束以后,它内部所在的空间肯定会有涉及到空间回收的情况,比如说obj1和obj2,在全局的地方我们已经不再去指向它俩,所以说这个时候它的引用计数应该是为0的,但是此时有一个问题,在函数里面我们会发现,当我们想要去来找GC把obj1删除的时候,它会告诉我们obj2有一个属性指向obj1的,虽然按照之前的规则,在全局的作用域下找不到了obj1和obj2,但是由于它们两者之间在这样一个作用域范围内明显还有一个互相指引关系,所以在这种情况下,他们的引用计数器的数值并不是为0 的,这个时候引用计数算法下的GC就没有办法将这样两个空间进行回收了,从而造成内存空间浪费,这就是是对象之间的循环引用。

  • 时间开销大:该算法的时间开销会大一些,因为当前的引用计数它需要去维护一个数值的变化,在这种情况下,它要时刻的监控着当前对象的引用数值是否需要修改,这个对象的数值的修改就要需要消耗时间,如果对象多,则时间会消耗较大

5、标记清除算法实现原理

  • 核心思想:分标记和清除二个阶段完成
  • 遍历所有对象找标记活动对象,活动对象跟可达对象是一个道理:在这个阶段当中,要去找到所有可达对象,如果说在这里引用了一个层次关系那么它回去递归的进行查找,就像global找A再找D的过程,找完以后就将这些可达对象进行标记

  • 遍历所有对象清除没有标记对象,同时将上一个阶段的标记给抹掉:标记完成以后,开始做清除,找到那些没有去做标记的对象,同时把做的标记清除,这样就完成了一次回收

  • 回收相应的空间:回收后会将空间放在当前的空闲列表上面,后续的程序可以直接在这申请空间使用

标记清除算法优点:

  • 可以解决对象循环引用的回收操作,在写代码的时候可能会在全局定义a,b,c这样的一个可达对象,但是我们也是会有一些函数的局部作用域比如当前在函数内定义了a1,b1,并让他们互相引用,对于这种函数的调用在结束之后必然要释放他们内部的空间,这种情况下一旦当某一个函数调用结束之后,它局部空间的变量就失去了与我们当前全局global在作用域上的连接,a1和b1在我们全局的global根下边就没办法访问到了,就是一个不可达对象,不可达对象在标记阶段是就不会完成标记,在第二个回收阶段时,当前回收的时候直接找到没有标记的对象,把它内部的空间清除释放,这优点是相对于引用计数算法来说的

标记清除算法缺点:

  • 空间碎片化:由于我们当前缩回手的这样一个垃圾对象,它在地址上不是连续的,我们回收之后,它们分散在各个角落,后续我们想要使用的话,如果刚好巧了,新的申请空间刚好与他们的大小匹配,那么就能直接用,一旦是多了还是少了,我们就不太适合使用了。要是在空间链表里申请了1.5个域的空间,那么前后两个回收的空间地址都不匹配。

 四、标记整理算法原理

  • 标记整理可以看做是标记清除的增强
  • 标记阶段的操作和标记清除一致
  • 清除阶段会先执行整理,移动对象位置

回收前标记好活动对象,整理时把活动的对象移动到一端,然后直接释放掉活动对象以外的内存

五、认识V8

  • V8是一款主流的JavaScript执行引擎
  • V8采用即时编译
  • V8内存设限(64位1.5GB,32位700MB)

六、V8垃圾回收策略

  • 采用分代回收的思想:主要就是把我们当前的内存空间去按照一定的规则分成两类。
  • 内存分为新生代、老生代存储区
  • 针对不同对象采用不同算法

V8中常用GC算法:

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量

详情后续介绍

七、V8如何回收新生代对象

  • V8内存空间一分为二
  • 小空间用于存储新生代对象(32M|16M):64位操作系统是32M,32位操作系统是16M的
  • 新生代指的是存活时间较短的对象:比如说在当前的代码内有一个局部的作用域,这个作用域当中的变量在执行完成之后就肯定要去回收,而在其他的地方比如全局里面也有个变量,它肯定要等到我们程序退掉之后才能被回收,所以新生代就是那些存活时间较短的对象

新生代对象回收实现

  • 回收过程采用复制算法+标记整理
  • 新生代内存区分为二个等大小空间:存活时间较短称为新生代对象
  • 使用空间为From,空闲空间为To
  • 活动对象存储于From空间
  • 标记整理后将活动对象拷贝至To
  • From与To交换空间完成释放

回收细节说明

  • 拷贝过程中可能出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 一轮GC还存活的新生代需要晋升
  • To空间的使用率超过25%:在将来回收操作的时候最终是需要把From空间和To空间进行一个交换,以前的To变成From,From变成To,意味着如果To使用率达到了80%,那么最终它变成活动对象的存储空间后,那么新的对象好像就存不进去了

V8如何回收老生代对象

  • 老年代对象存放在右侧老生代区域
  • 64位操作系统1.4G,32操作系统700M
  • 老生代对象就是指存活时间较长的对象

老生代对象回收实现

  • 主要采用标记清除、标记整理、增量标记算法
  • 首先使用标记清除完成垃圾空间的回收
  • 采用标记整理进行空间优化
  • 采用增量标记进行效率优化:当垃圾回收进行工作的时候,其实会阻塞JavaScript程序的执行,这样会有一个空档期,例如程序执行完成之后,那么会停下来,去执行当前的回收操作。标记增量就是将我们当前一整段的垃圾回收操作拆分成多个小步骤组合着去完成当前整个回收,从而去替代我们之前一口气完成的垃圾回收。下图非常明显,主要让我们实现垃圾回收与程序执行交替着完成,而不像以前程序执行的时候的不能做垃圾回收,这样时间消耗更合理一些

细节对比

  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适合复制算法:老生代存储空间是比较大的,如果说一分为二,那么基本上几百M的空间是浪费不用的,这样的话就太奢侈了。老生代存储区域中所存放的对象数据是比较多,复制过程中消耗时间非常多,所以不适合。

先分享到这里,下篇介绍Performance工具

猜你喜欢

转载自blog.csdn.net/weixin_41962912/article/details/110048339