JavaScript性能优化

·  阅读 180

内存管理

内存为什么需要管理

  • 如果我们在编程的时候不够了解内存管理的机制,容易让我们编写一些不容易察觉到的内存问题性代码,这种代码多了以后,给我们程序带来的可能是一些意想不到的bug,掌握内存管理是非常有必要的。

内存管理介绍

  • 内存:由可读写单元组成,表示一片可操作空间
  • 管理:人为的去操作一片空间的申请,使用和释放
  • 内存管理:开发者主动申请空间,使用空间,释放空间
  • 管理流程:申请-使用-释放

JavaScript中的内存管理

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

JavaScript中的垃圾回收

JavaScript中的垃圾

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

JavaScript中的可达对象

  • 可以访问到的对象就是可达对象(引用、作用域链)

  • 可达的标准就是从根出发是否能够被找到

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

    /**
     * 可达对象示例
     */
    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)
    复制代码

    输出如下:

GC算法介绍

GC里的垃圾是什么

  • 程序中不再需要使用的对象

    • function func() {
          name = 'xzz'
          return `${name} is a coder`
      }
      
      func()
      复制代码
  • 程序中不能再访问到的对象

    • function func() {
          const name = 'xzz'
          return `${name} is a coder`
      }
      
      func()
      复制代码

GC算法是什么

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

常见GC算法

  • 引用计数
    • 可以通过一个数字来判断当前的对象是不是一个垃圾
  • 标记清除
    • 可以在进行工作的时候给到那些活动对象添加上一个标记来判断它是否是一个垃圾
  • 标记整理
    • 标记整理和标记清除很类似,在我们后续回收的过程中可以做出一些不一样的事情
  • 分代回收
    • V8当中会用到这个回收机制

引用计数算法

实现原理

  • 核心思想:设置引用数,判断当前引用数是否为0
  • 引用计数器
  • 引用关系改变时修改引用数字
  • 引用数字为0时立即回收

引用计数算法优点

  • 可以及时回收垃圾对象(发现垃圾立即回收)
    • 可以根据当前引用数是否为0来决定一个对象是不是垃圾,如果为0会立即释放
  • 最大限度减少程序暂停
    • 我们的应用程序在执行的过程中必然的会对内存进行消耗,而我们当前的执行平台它的内存肯定是有上限的,所以内存肯定有占满的时候,不过由于引用计数算法它是时刻监控着那些引用数值为0的对象,所以我们就可以认为当它发现这个内存即将爆满的时候那么引用计数会立马找到那些数值为0的对象空间对其进行释放,所以这样就保证了我们当前的内存不会有占满的时候,这就是所谓的减少程序暂停的说法

引用计数算法缺点

  • 无法回收循环引用的对象

    • function fn() {
          const obj1 = {}
          const obj2 = {}
      
          obj1.name = obj2
          obj2.name = obj1
      
          return 'xzz is a coder'
      }
      
      fn()
      复制代码
  • 资源消耗较大,时间开销大

    • 因为我们当前的引用计数它需要去维护一个数值的变化,所以在这种情况下它要时刻的监控着当前对象的一个引用数值是否需要修改,本身来说对象数值的修改就需要消耗时间,那如果我们这个内存里面有更多的对象需要修改,那么这个时间就会显得更大一些,所以对比其他GC算法来说,引用计数算法的时间开销会更大
    • 需要维护一个引用计数器,每次操作都要去修改这个引用数,而这个空间的引用数有可能很大,也有可能很小,总之频繁的操作会有资源上的开销

总结

  • 核心思想就是在内部去通过一个引用计数器来维护每一个对象都存在的一个引用数值,通过这个数值是否为0来判断这个对象是否是一个垃圾对象,如果是垃圾对象让垃圾回收器来进行一个回收和释放

标记清除算法

实现原理

  • 核心思想:分标记和清除二个阶段完成
  • 遍历所有对象找标记活动对象
  • 遍历所有对象清除没有标记对象
  • 回收相应的空间

标记清除算法图示

  • 在第一个阶段当中我们要找到所有可达对象,如果说在这里涉及到了我们这样一个层次关系那么它会递归的去进行查找,就好像我们的global找A在找D这样的一个过程,那么找完以后它会将这些可达对象进行标记。标记完成以后会进行第二个阶段开始进行清除,找到那些没有被标记的对象,同时也会将我们之前第一次所做的标记清除掉,这样我们就完成了一次垃圾的回收,同时我们还要留意最终它还会去把回收的空间直接放在我们当前的一个叫作空闲链表上面,方便我们后续的程序可以直接在这申请空间使用。

标记清除算法优点

  • 相当与引用计数来说,标记清除具有一个最大的优点,它可以去解决我们之前对象循环引用的一个回收操作。

标记清除算法缺点

  • 空间碎片化,不能让我们的空间得到最大化的使用
  • 不会立即回收垃圾对象,即使在遍历的过程中它发现了这样的一个对象是不可达的,但是它也要等到最后才去清除,而且它去清除的时候当前的程序是停止工作的

模拟内存存储情况:

我们当前从根去进行查找,在下方它有一个可达对象(红色标注区域),我们认为这是A对象,然后紧接着它左右两侧有一个从根下无法直接查找的区域,例如左侧我们称之为B,右侧我们称之为C,这种情况下在进行第二轮清除操作的时候它就会直接将我们当前的B和C所对应的空间进行回收,然后在把我们释放的空间添加到空闲链表之上,紧接着我们后续的程序就可以直接进来再从空闲链表上去申请相应的空间地址进行使用。在这种情况下有一个问题,举例说明一下,比如我们当前认为任何一个空间都由两个部分组成,一个是存储这个空间的一些原信息的,它的大小,它的地址(头)。再或者它还有一个部分是专门用于存放数据的,我们叫做域,这样说完以后,我们左右两侧这样一个空间我们认为B对象有2个字的空间而C对象有1个字的空间,那么在这种情况下我们虽然对它进行了回收,加起来好像释放了3个字的空间,但是由于它们中间被我们的A对象分割着,在释放完成之后它们其实是分散的,也就是地址不连续。在这种情况下如果后续我们想去申请一片空间,而刚好巧了我们这一次申请的空间大小刚好是1.5个字,这种情况下如果我们直接找到B所释放的空间会发现是多了0.5个,但如果我们直接去找C所释放的空间它又不够,因为C只有1个空间,就造成标记清楚算法最大的问题,空间的碎片化(所谓的空间的碎片化是由于我们当前所回收的垃圾对象在地址上它本身是不连续的,由于这种不连续从而造成了我们在回收之后它们分散在各个角落),那后续我们要想去使用的时候,如果刚好巧了,新的生成空间刚好与它们的大小匹配那么我们可以直接使用,但多了或者少了我们就不太适合使用了,所以这就是我们标记清除算法当作最大的缺点,我们称作为空间碎片化。

总结

  • 这种算法分两个阶段来进行,首先它会去遍历所有的对象,然后去给当前的活动对象进行标记,紧接着会去清除掉那些没有被标记的对象,从而去释放这些垃圾的所占用的空间

标记整理算法

实现原理

  • 标记整理可以看做是标记清除的增强
  • 标记阶段的操作和标记清除一致
  • 清除阶段会先执行整理,移动对象位置去让它们能够在地址上产生连续

标记整理算法图示

  • 回收前:我们有很多的活动对象和非活动对象以及一些空闲的空间,那么当它去执行我们当前标记操作的时候就像我们看到的会把所有的活动对象进行标记
  • 整理后:进行一些整理的操作,整理的是什么,在这就是我们看到的一个位置上的改变,它会去把我们当前的活动对象先去进行移动,在地址上变成连续的位置。
  • 回收后:将当前活动对象右侧的范围进行整体的回收,回收完成以后我们就会得到回收后(图示)的情况,这种情况针对于我们之前的标记清除算法来说它的好处显而易见,因为我们现在内存里面就不会大批量的出现那些分散的小空间,而回收到的空间基本上都是连续的,在后续的使用过程中如果我们想要去申请的时候就可以尽可能的去最大化利用我们当前内存中所释放出来的空间。

标记整理算法优缺点

  • 优点
    • 减少碎片化时间
  • 缺点
    • 不会立即回收对象

总结

  • 标记整理的做法和标记清除的做法类似,只不过它的做法有一些前置的操作要先去整理一下当前的地址空间

V8引擎的垃圾回收

认识V8

  • V8是一款主流的JavaScript执行引擎
    • 我们日常使用的浏览器以及目前的NODE平台都是在采用这样一个引擎去执行我们的JavaScript代码
  • V8采用即时编译
    • 之前很多JavaScript引擎需要将源代码转换成字节码然后才能去执行,对于V8来说就可以直接将源码翻译成机器码,这个时候的速度是非常快的
  • V8内存设限
    • 64位操作系统上这个数值不操作1.5G
    • 32位操作系统上这个数值不操作800M

V8垃圾回收策略

  • 采用分代回收的思想
  • 内存分为新生代,老生代
  • 针对不同对象采用不同算法

V8中常用的GC算法

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

V8内存分配

  • V8内存空间一分为二
  • 小空间用于存储新生代对象(32M|16M)
  • 新生代指的是存活时间较短的对象

新生代对象回收实现

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

回收细节说明

  • 拷贝过程中可能出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 一轮GC还存活的新生代需要晋升
  • To空间的使用率超过25%

总结:

新生代对象的存储区域被一分为二,而且是两个同等大小的,在两个等大小的空间中我们起名From和To,当前我们使用的是From,所有的对象声明都会放在From空间内,然后触发这些机制的时候我们就会把这些活动对象全部找到进行整理拷贝到To空间当中,拷贝完成以后我们再让From和To去进行空间的交换,这样我们就算是完成了空间的释放和回收操作。

老年代对象说明

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

老年代对象回收实现

  • 主要采用标记清除,标记整理,增量标记算法
  • 首先使用标记清除完成垃圾空间的回收
  • 采用标记整理进行空间优化
  • 采用增量标记进行效率优化

细节对比

  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适用复制算法

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

  • 将一整段垃圾回收操作拆分成多个小部分执行,实现垃圾回收玉程序执行交替进行,组合完成整个垃圾回收。

总结

  • V8是一款主流的JavaScript执行引擎
  • V8内存设置上限
  • V8采用基于分代回收思想实现垃圾回收
  • V8内存分为新生代和老生代
  • V8垃圾回收常见的GC算法,新生代主要采用复制算法和标记整理算法,老生代主要采用标记清除,标记整理,增量标记三个算法

Performance工具

代码优化实例

总结

  • JavaScript中的垃圾回收就是找到垃圾,然后让JavaScript的执行引擎来进行空间的释放和回收。
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改