js中的垃圾回收和性能

73 阅读5分钟

垃圾回收

  • 标记清理

    1. 当变量进入上下文,这个变量会被加上存在域上下文中的标记,当变量离开上下文,加上离开上下文的标记

    2. 当垃圾回收程序运行的时候,会标记内存中储存的所有变量,然后将所有在上下文中的变量,以及被上下文中变量所引用的其他变量的标记去掉。

    3. 垃圾回收程序去掉所有变量的标记之后,哪些还留存有标记的变量就该被回收,因为此时已经没有任何上下文在直接或间接的使用它们

  • 引用计数

    1. 对每个值,当被引用时就使他的引用次数加一
    2. 当引用一个值的变量被重新赋值,引用次数减一
    3. 当引用次数为0,就该被垃圾回收

    缺陷:循环引用时无法回收内存

内存管理

  1. 解除引用

    优化内存占用的最佳手段就是保证在执行代码的时候只保存必要的数据,如果数据不在必要就把它设置为null,从而释放其引用(解除引用)。这个方式最适合全局变量和全局对象的属性,局部变量在上下文推出时就会自动被解除引用

  2. 隐藏类和删除操作

    • 隐藏类

      V8 在将解释后的JavaScript 代码编译为实际的机器码时会利用隐藏类

      代码运行期间,V8会将创建的对象和隐藏类关联起来,以追踪它们的属性特征,能狗共享相同隐藏类的对象性能会更好。

      例:

      function Article() {
      	this.title = 'Inauguration Ceremony Features Kazoo Band';
      }
      // 对于a1,a2两个实例,V8会让这两个实例共享相同的隐藏类
      // 因为这两个实例的构造函数和原型是同一个
      let a1 = new Article();
      let a2 = new Article();
      
      // 假如,给a2动态赋值一个属性,a1,a2又会对应两个不同的隐藏类了
      a2.author = 'jack'
      

      对于上面示例代码这种情况:多个实例先是共享一个隐藏类的,然后由于某种原因,又不共享一个隐藏类了 ————> 这种情况太多的话就有可能对性能产生显著影响。

      对于这种情况,解决方案很简单,就是要避免先创建在补充的动态赋值行为,并在构造函数中一次性声明所有属性。

    • 删除操作

      与动态给对象添加属性类似,使用delete关键字动态删除属性也会导致多个本应共享同一个隐藏类的实例又生成了相同的隐藏类片段

      例:

      function Article() {
        this.title = 'Inauguration Ceremony Features Kazoo Band';
        this.author = 'Jake';
      }
      // a1和a2创建的时候是共享相同的隐藏类的
      let a1 = new Article();
      let a2 = new Article();
      // 动态删除a1的属性后,V8又为a1重新生成新的隐藏类
      delete a1.author;
      

      删除属性的最佳实践应该是给不需要的属性赋值为null,这样既保持了隐藏类不变和继续共享,同时也让垃圾回收程序可以回收不用的对象内存了

内存泄漏

写得不好的JavaScript 可能出现难以察觉且有害的内存泄漏问题。

JavaScript 中的内存泄漏大部分是由不合理的引用导致。

  1. 意外声明全局变量

    function add(a, b) {
      // sum没有用任何关键字声明,会导致sum变成一个全局变量,只有全局上下文结束后才会被释放
      // 但是很显然 sum的生命周期只应该存在这个函数的作用域内
      // 如果这里意外声明的是一个复杂对象就有可能造成内存泄漏
      sum = a + b
      return sum
    }
    
  2. 未及时清理的定时器可能会造成内存泄漏

    let name = 'Jake';
    // 这个定时器如果一直没有被释放,那么name也永远不会被释放
    setInterval(() => {
    	console.log(name);
    }, 100);
    
  3. 闭包内变量一直没有被释放

    function createSumFunc(a) {
      return (b) => {
        return a + b
      }
    }
    // 这个函数创建后,createSumFunc的参数a就会一直存在与内存中供add3这个函数使用
    // 如果add3的引用一直没有释放,那么a的内存就也一直不会释放
    // 所以add3使用完成后就应该把add3置为null,或者让add3永远值创建在很快就结束的局部上下文当中
    const add3 = createSunFunc(3)
    

静态分配和对象池

对于浏览器来说,垃圾回收程序同样也是要耗费很多性能的,所以应该想办法减少垃圾回收的次数。

但是开发者是没有办法直接控制垃圾回收的时机的,根据垃圾回收的原理可以间接的来控制出发垃圾回收的条件。

浏览器决定合适运行垃圾回收的一个标准是对象更替的速度,假如代码中短时间又很多复杂对象被初始化,然后又马上使用完成后被置为null。或超出作用域了,那么浏览器势必会采用更激进的方式来调度垃圾回收程序,才能使剩余内存始终够用,这样对性能肯定有很大的影响。

例:

function addVector(a, b) {
  // vector假设使一个很复杂的对象
  // addVector这个函数一直在被频繁调用
  // 那么就会有很多vector的实例在不断的被创建和失效,这样就会导致垃圾回收程序更激进的运行影响性能
  let resultant = new Vector();
  resultant.x = a.x + b.x;
  resultant.y = a.y + b.y;
  console.log(resultant.x, resultant.y)
}

要解决上面的问题,就需要一个静态分配策略,不在函数调用时直接创建新的对象

最容易想到的方法就是对象池

根据需要,可以在程序初始化时就实例化好若干可能用到的对象,当有其他地方需要用到该中类型的对象时就直接拿过来用就行,而不用始终一直实例化新的对象。对象池的大小也可以根据不同的需求,制定好不同的策略来动态更改

function addVector(a, b) {
  // 从对象池中拿到已经实例化好的对象,不用重新实例化一个新的对象
  let resultant = vectorPool.getVector();
  resultant.x = a.x + b.x;
  resultant.y = a.y + b.y;
  console.log(resultant.x, resultant.y)
}