垃圾回收

89 阅读5分钟

垃圾回收

内存空间

JavaScript 的执行过程中,有三种类型的内存空间

  • 代码空间:存放可执行代码
  • 栈空间:一块连续的内存区域,容量较小,读取速度快,被设计成先进后出结构,即 JavaScript 的调用栈,用来存储执行上下文和存储在执行上下文中的小数据
    • 创建执行上下文的过程:
      • 1.创建环境变量和词法环境
      • 2.建立作用域链
      • 3.初始化变量和函数的声明(仅 var 变量,let 和 const 声明的变量不会在创建阶段被初始化)
      • 4.代码执行
  • 堆空间:不连续的内存区域,容量较大,用于存储大数据,读取较慢

闭包

调用一个外部函数返回一个内部函数后,即使该外部函数一句执行结束了,内部函数引用外部函数的变量依然保存在内存中,这些变量的集合称为闭包(Closure(fn) 可以在 debugger 页面看到)

而且,任意外部函数 fn 产生的闭包 Closure(fn) ,其所有内部函数都会拥有一个执行这个闭包的引用,所有内部函数共享同一个闭包,下面例子测试后可以看到,add 函数执行时,能在开发中工具中看到闭包 Closure(outter) 中看到 count 和 obj1

<script>
  function outter() {
    const obj1 = { a: 1 };
    let count = 1;

    function add() {
      debugger;
      return count++;
    }
    function inc() {
      debugger;
      return (count = count - obj1.a);
    }
    return { add, inc };
  }
  const { add, inc } = outter();
  add();
  inc();
  console.log(add());
</script>

内存生命周期

所有编程语言内存的生命周期都是差不多的:申请内存 --- 使用内存(读写) --- 释放或归还内存

C/C++ 等底层语言,使用者需要手动申请内存空间,使用完毕再释放内存

现代的编程语言类似 JavaScript 则会自动进行这些流程

64 位系统上的 chrome 浏览器单个 tab 的内存上限为 1.4GB 左右,32 位系统为 512MB (1GB = 1B * 1024 * 1024 大概 100 万字节)

JS 中的变量存储和垃圾回收

原始类型直接存储在 栈内存(Stack) 中,引用类型存储在 堆内存(Heap) 中

:::warn 如果字符串长度非常大,也会直接存放在堆内存中,栈内存大小有限,同理 [-2^31 到 2^31 - 1 的整数] 之外的数字也是存放在堆内存中,那 bigInt 呢?固定的 undefined / null / true / false 呢?是按照调试结果来还是按照源码来? :::

闭包中的变量也存储在堆内存中 (其中的非引用类型,在执行栈栈出上下文时,怎么存放到堆中,这个从栈移动到堆的过程) 全局作用域的变量存储在全局对象上(全局对象存储在哪里?答:一直存在内存中,直到页面关闭,刷新页面也不会释放,比如 window global ,存放在堆内存)

函数执行完毕,该函数执行上下文从栈中弹出,存储在执行上下文中的变量立即被回收掉,此时原始类型都被销毁回收,但是引用类型销毁掉只是变量对堆内存地址的引用,引用对象本身还在堆内存中,此时就需要堆内存垃圾回收机制了

存放在栈中还是堆中都是由 js 引擎处理的

代际假说:认为大部分新对象生存时间比较短,在一次垃圾回收周期内被回收

基于此,V8 将堆内存分为新生代和老生代,新生代又分为 Nursery(from) 和 Intermediate(to) 两个区域 新对象存放在 Nursery 区域,经过一次垃圾回收,存放的对象被复制到 Intermediate 区域,经过两次垃圾回收

主垃圾回收器 Major GC

  • 识别活动对象 marking

通常是从一个根对象进行递归遍历,所有遍历到的对象都是可达的,为活动对象,没有遍历到的对象为非活动对象,需要进行回收

  • 回收或重用垃圾对象内存 sweeping

GC 会维护一个 freeList 对象,将非活动对象占用的内存片段地址添加到 freeList ,有新对象申请内存时 freeList 中有合适大小的内存块,会优先分配给新对象

  • 整理碎片内存 defragment

内存经过垃圾回收之后,活动对象将内存分割的很零碎,这个时候会进行整理,把活动对象复制到相同连续的区域内

副垃圾回收器

  • 标记

  • 复制

  • 更新指针

  • 切换角色

新生代将内存分为 from space(Nursery)和 to space(Intermediate),当有新对象申请内存时,会分配 from space 区域中的地址 给活动对象标记,然后复制到 to space,更新指针引用地址,然后切换 from 和 to 角色,存活两次的对象会被复制到老生代区域

  • 并行:主线程进行垃圾回收任务时,开几个辅助线程同时进行,大大减少主线程全停顿时间

  • 增量:将主线程垃圾回收任务拆分成多个小任务,与 js 交替执行,给了 js 相应高优任务的时间,避免卡顿

  • 并发:主线程专注执行 js,开启辅助线程进行垃圾回收,没有全停顿

如何减少 GC 的工作负担

  • 避免使用全局变量:全局变量会一致存在于内存中,直到页面关闭

  • 避免循环引用:对象之间循环引用,即使没有其他引用指向他们,垃圾回收器也无法回收

  • 避免频繁的大内存分配:频繁创建大内存对象会增加 GC 的负担,尽量重用

  • 对于不再使用的对象,可以手动清除(存疑)

  • 事件监听器及时移除(也可以避免意料之外的触发)

  • 使用对象池,如果需要频繁创建和销毁,比如以前的 React 事件池