​ 在 JavaScript 中,原始数据类型是存储在栈区的,引用数据类型存储在堆区。在代码执行的过程中,一部分数据在被使用之后就不在被需要了,这些数据就变成了垃圾数据。如果不清除它们,内存消耗会越来越多。因此,需要对这些垃圾数据来做回收,释放有限的空间。

垃圾回收策略
  • 手动回收 -- C/C++ free()
    • 如果一段数据不再使用,又没有回收,就会造成内存泄漏
  • 自动垃圾回收策略 -- Python、Java、JavaScript 等
    • 产生的垃圾由垃圾回收器释放
JavaScript 中的垃圾回收机制
  • 调用栈

    • 调用栈中会有一个记录当前执行状态的指针(ESP:类似于数组实现的栈中记录栈顶的索引,弹栈时直接下移,入栈时如果新的位置之前有元素直接覆盖),当前代码块执行完毕后下移。
    • JavaScript 引擎通过向下移动 ESP 来销毁保存在栈中的执行上下文
    • 通过 JavaScript 中的垃圾回收器来回收堆中的垃圾数据

    • 代际假说 -- Java、JavaScript、Python 等等

      • 特点
        • 大部分对象在内存中的存在时间很短。即,很多对象在分配内存之后,很快就变得不可访问
        • 不死的对象,活得更久
    • V8 的实现

      • 将堆分为新生代和老生代两个区域

        • 新生代:存放生存周期短的对象 (容量 1 ~ 8 M)
        • 老生代:存放生存时间久的对象(容量比起新生代大得多)
      • V8 使用两个不同的垃圾回收器来实现高效的垃圾回收

        • 主垃圾回收器:负责老生代的垃圾回收
        • 副垃圾回收器:负责新生代的垃圾回收
      • 垃圾回收器的工作流程(垃圾回收器的工作流程相同,无论何种类型的垃圾回收器)

        1. 标记空间中的活动和非活动对象。
          • 活动对象:还在使用的对象
        2. 回收非活动对象占据的内存。 -- 在标记完成之后,统一清理内存中所有被标记为可回收的对象
        3. 内存整理
          • 频繁回收对象,内存中会出现大量的不连续空间 -- 内存碎片。如果内存中空间碎片较多,分配较大的连续空间的时候,可能会出现内存不足的情况。
      • 主垃圾回收器 -- 老生代

        • 除了新生区中晋升的对象,一般情况下,大的对象会直接分配到老生区。

        • 老生区中对象的特点

          1. 对象占用空间大
          2. 对象存活时间长
        • 由于对象较大,采取与新生区中相同的策略会导致回收的执行效率低下,同时也会浪费一半的空间。因此,主垃圾回收器采用 标记 -- 清除 来进行垃圾回收

        • 回收过程
          1. 从一组根元素开始,可以到达的元素是活动对象,不可到达的是垃圾数据( 调用栈中 ESP 下移之后,当前 ESP 上面的执行上下文 ) -- 标记
          2. 清除 -- 导致内存碎片
          3. 整理内存(标记 -- 整理算法:标记过程同标记--清除算法,之后让所有存活的对象向一端移动,然后直接清理掉端边界之外的内存

      • 副垃圾回收器 -- 新生代

        • 通常,大多数小的对象会被分配到新生区。新生区中垃圾回收比较频繁。
        • 使用 Scavenge(清除) 算法处理
          • 现将新生代对半划为两个区域:对象区域、空闲区域
          • 新加入的对象存放到对象区域,对象区域快被写满时,执行一次垃圾清理操作。
            • 首先标记对象区域中的垃圾
            • 在垃圾清理过程中,副垃圾回收器将对象区域中存活的对象复制到空闲区域,并且有序的排列起来 -- 内存整理,复制后空闲区域没有内存碎片
            • 复制操作完成后,对象区域和空闲区域角色翻转,完成垃圾对象的回收。翻转的操作可以使新生代中的两块区域无限重复的使用下去
          • 每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。由于复制需要时间成本,如果新生区的空间设置的过大,会导致每次清理的时间过久。为了执行效率,一般新生区的空间会设置的比较小
          • 新生区的空间较小,很容易被对象装满。JavaScript 引擎采取对象晋升策略来解决这个问题 -- 经过两次垃圾回收依然存活的对象会被移动到老生区中
    • 全停顿

      • JavaScript 运行在主线程之上,一旦执行垃圾回收算法,需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕之后恢复脚本的执行。 -- 这就是全停顿

      • V8 新生代的垃圾回收由于空间较小,存活对象较少,全停顿影响不大

      • 老生代中垃圾回收耗时较长,在这段时间中主线程不能做其他的事情。就会导致出现卡顿的现象

        • 为了解决这个问题,V8 将标记过程拆分为一个个子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直至标记完成。 -- 增量标记算法

        • 将一个完整的垃圾回收任务拆分成很多小的任务,小任务执行时间短,可以穿插在 JavaScript 任务中执行

        • 没有完美的解决方案,只能两害相权取其轻,两利相劝取其重