JavaScript 性能优化篇(一)内存管理

115 阅读5分钟

前言

本篇文章主要从JavaScript内存管理的基本原理出发讲解了内存的基本操作流程和垃圾对象的判定标准以及主流的垃圾回收机制GC算法的原理和对应优缺点。

1、内存管理的基本流程

内存管理介绍: 内存是由可读写单元组成的一片可操作空间,由人为去操作这片空间的申请、使用和释放。

JavaScript内存管理流程和其他语言一样, 内存管理基本流程是申请-使用-释放

// 申请
let obj = {};
// 使用
obj.name = "Hello Buddy!";
// 释放
obj = null;

2、垃圾对象

  • 对象不再被引用
  • 对象不能从根上访问到

JavaScript内存管理是自动的,对象不再被引用是垃圾,对象不能从根上访问到时是垃圾,执行引擎对垃圾对象的内存释放就是垃圾回收。 这里还涉及到一个概念叫“可达对象”:就是可以访问到的对象,可达的标准是从根出发是否能够被找到(通过引用和作用域链),这个根可以理解为全局变量对象。

可达对象

  • 可以访问到的对象
  • 从根出发是否能够被找到(通过引用和作用域链)
  • 根可以理解为全局变量对象

如下代码所示: 浏览器环境中,根对象就是在就是window对象,它的let obj 属性,引用着通过makeObj()创建的对象。

function makeObj(obj0,obj1) {
   obj0.next  = obj1;
   obj1.pre  = obj0;
   return {
     left:obj0,
     right:obj1
   }
}

let obj = makeObj({name:'obj0'},{name:'obj1'})

// 可以通过以下访问链访问 obj0 和 obj1
console.log(obj.left.name); // obj0
console.log(obj.left.next.name);  // obj1
console.log(obj.right.name); // obj1
console.log(obj.right.pre.name); // obj0

// 如果设置如下
obj.left = null;
obj.right.pre = null;
// 则此时的obj0 则为垃圾对象 ,因为它访问不可达 

引用关系如图:

WX20220912-111701@2x.png

断开obj0 的引用obj.leftobj.pre 变为不可达对象

obj.left = null;
obj.right.pre = null;

此时的引用关系如图:

WX20220912-111835@2x.png

3、垃圾回收机制 GC 算法

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

  • GC是一种机制,由垃圾回收器完成具体工作。
  • 负责查找垃圾释放、回收内存空间。

常见的GC算法

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

3.1、引用计数算法

引用计数算法的核心思想是 引用计算器设置对象引用计数,当该对象的引用计数值等于0的时候会触发垃圾GC机制回收释放。

  • 引用计数器
  • 设置引用计数,并且判断计数值是否为0
  • 引用计数值为0时立即回收

function func() { 
   let obj = {
      name:'hello!'
   };
   console.log(obj.name);
}
// 调用函数压入调用堆栈,申请对应上下文内存空间,上下文持有obj对象 引用计数+1
func();
// 调用完后,函数调用弹出调用栈, obj就不再被引用,引用计数-1,引用计数为0,从而被回收。

// 该函数产生一个闭包
function gen_closure() {
   let obj = {
      name:'hello!'
   }; 
    return function(){
        console.log(obj.name);
    }
}
// 调用函数
let closure = gen_closure();
// 注意这里调用完`gen_closure`函数 obj 对象并没有被回收。引用计数不为0,此时被 closure 变量持有的闭包所捕获而被引用。
// 调用闭包
closure();
// 此时闭包上下文被弹出调用栈,上下文被释放,obj对像引用计数变为0,最终也被回收。

引用计数的优缺点

引用计数需要实时监测对象引用计数值,发现垃圾立即回收,最大限度减少了程序暂停,但是另一方面就大大增加了时间开销。另外一个缺点是引用计数算法不能回收循环引用的对象。

总结就是:

  • 发现垃圾立即回收
  • 最大限度减少了程序暂停
  • 时间开销大
  • 不能回收循环引用的对象

循环引用的产生

function func(){
    let obj0 = {};
    let obj1 = {};
    obj0.right = obj1;
    obj1.left = obj0;
}
func();
//调用完func后obj0和obj1不会被回收。

调用完func()obj0obj1没有被回收,这里就是产生了循环引用导致的,此时各自的引用计数为1。

WX20220912-131150@2x.png

3.2、标记清除算法

标记清除算法的核心思想是 分标记和清除两个阶段完成。第一个阶段遍历所有对象找到并且标记活动的对象;第二个阶段遍历所以对象清除那些没有标记的对象。

  • 分标记和清除两个阶段完成
  • 遍历所有对象找到并且标记活动的对象
  • 遍历所以对象清除那些没有标记的对象

标记过程是通过遍历算法从根出发找可达对象,将其标记为活动对象。不可达的不被标记,在第二个阶段被清除回收,并同时清除上次的标记。如图:A、B、C、D、E 从根出发是可访问的,所以可达,会被标记为活动对象,而F、G对象即便是产生了循环引用,但是不可达,所以不会被标记,会在第二个阶段被清除回收,回收的内存地址会被加入到空闲链表等待下次的使用。

WX20220912-140825@2x.png

标记清除算法的优缺点

不难看出相对于引用计数算法,标记清除算法有个最大的优点就是解决了循环引用不能被回收问题,但是缺点就是释放的内存空间加入空闲链表后并不是连续的地址空间导致碎片化空间的问题。

  • 不存在循环引用不被回收的问题
  • 会造成碎片化的空间
  • 不会立即回收垃圾对象

3.3、标记整理算法

标记整理算法是标记清除算法的增强版,标记阶段和清除阶段一致。不同点是标记整理算法会在清除阶段移动整理回收的内存空间,让地址产生连续,解决内存碎片问题。如下图:

WX20220912-144839@2x.png

标记清除算法的优缺点

  • 减少了碎片化的空间
  • 不会立即回收垃圾对象

好啦,这就是JavaScript内存管理的基本原理。下一章节我们将介绍有关V8引擎的回收策略分代回收机制。