JavaScript 内存管理于我们来说是自动的、不可见的。我们创建的原始类型、对象、函数等等,都会占用内存。当这些数据不被需要后会发生什么?JavaScript 引擎如何发现并清除他们?

可触及(Reachability)

JavaScript 内存管理的关键概念是可触及(Reachability)

简单来说,“可触及”的值就是可访问的,可用的,他们被安全储存在内存。

  1. 以下是一些必定“可触及”的值,不管出于任何原因,都不能删除: 当前函数的局部变量和参数。 当前调用链(current chain of nested calls)中所有函数的局部变量和参数。 全局变量。 (以及其他内部变量) 这些值都称为 root。
  2. 其他值是否可触及视乎它是否被 root 及其引用链引用。 假设有一个对象存在于局部变量,它的值引用了另一个对象,如果这个对象是可触及的,则它引用的对象也是可触及的,后面会有详细例子。

JavaScript 引擎有一个垃圾回收后台进程,监控着所有对象,当对象不可触及时会将其删除。

一个简单例子

// user 引用了一个对象
let user = {
  name: 'John',
}

箭头代表的是对象引用。全局变量 "user" 引用了对象 {name: "John"}(简称此对象为 John)。John 的 "name" 属性储存的是一个原始值,所以无其他引用。

如果覆盖 user,对 John 的引用就丢失了:

user = null

现在对象 { name: 'John' } 就会变得不可触及,垃圾回收机制就会对其进行删除并释放内存。

多个引用

如果现在有多个变量指向同一个对象,例如

let user = {
  name: 'John',
}
admin = user;
user = null;

1c7e8667-0d78-4cee-b9e8-28527639b3f7.png

这时候我们还是把 user 重置为 null,结果就不一样了,由于 admin 维持着 { name: 'John' } 引用,所以我们还得把 admin 重置为 null,垃圾回收机制才能对该对象进行删除并释放内存。

垃圾回收算法

引用计数

引用计数是一种内存管理技术,它跟踪每个对象被引用的次数,并在引用计数变为零时立即回收对象的内存空间。具体来说,引用计数器会记录每个对象被引用的次数,当有新的引用指向对象时,引用计数加一;当引用失效或对象不再被使用时,引用计数减一。当引用计数减为零时,表示该对象不再被任何引用指向,即可被回收。

引用计数的优点是可以立即回收不再被引用的对象,避免了内存占用过多的情况。然而,引用计数也存在一些缺点,比如无法处理循环引用的情况,导致循环引用的对象永远无法被回收,从而造成内存泄漏。因此,在实际应用中,引用计数通常会和其他垃圾回收算法结合使用,以解决各自的不足之处。

标记 - 清除算法

标记-清除算法通过标记和清除两个阶段来回收不再使用的内存空间。

在标记阶段,算法会从根对象开始,遍历所有可访问的对象,并将它们标记为活动对象。这个过程可以通过对象之间的引用关系来追踪对象的可达性,从而确定哪些对象是活动的。

在清除阶段,算法会遍历整个内存空间,清除所有未被标记的对象,这些未被标记的对象就是不再使用的垃圾对象。清除完成后,内存中就只剩下被标记的活动对象,而未被标记的垃圾对象所占用的内存空间就可以被释放出来。

尽管标记-清除算法能够有效地回收不再使用的内存,但它也存在一些缺点,比如内存碎片的问题,即清除后可能会留下不连续的内存空间碎片。为了解决这个问题,通常会结合其他算法,如标记-整理算法,来进一步优化内存的利用。

标记清除算法
标记清除算法

标记 - 整理算法

标记-整理算法是主要用于解决内存碎片问题。在标记-整理算法中,首先会执行与标记-清除算法相似的标记阶段,从根对象开始,标记所有活动对象。一旦标记完成,接下来的步骤与标记-清除算法有所不同。在整理阶段,算法会将所有活动对象向内存的一端移动,然后清理掉移动后端的所有垃圾对象,从而减少内存碎片。

通过整理阶段,标记-整理算法可以将内存中的活动对象紧凑地排列在一起,从而减少了内存碎片的产生。这有助于提高内存的利用率和减少内存分配的开销。

虽然该算法能够有效地解决内存碎片的问题,但它也会增加额外的内存移动操作,可能会导致一些性能开销。因此,在选择垃圾回收算法时,需要根据具体的应用场景和需求权衡各种算法的优缺点。

分代回收

分代回收根据对象的存活时间将内存分为不同的代,一般将内存分为年轻代和老年代。这种算法是基于一种观察:大多数对象在其生命周期中很快就会变得不可达,只有一小部分对象会长时间存活。

分代回收算法的核心思想是根据对象的存活特性采取不同的回收策略。具体来说,年轻代中的对象生命周期较短,通常采用效率较高的垃圾回收算法,如复制算法。而老年代中的对象生命周期较长,通常采用更稳定但效率较低的垃圾回收算法,如标记-清除算法或标记-整理算法。

在分代回收算法中,一般会有三个代:Eden区、Survivor区和老年代。新创建的对象会被分配到Eden区,经过一段时间后,仍然存活的对象会被移到Survivor区。在Survivor区经过多次存活后,对象会被晋升到老年代。通过这种方式,分代回收算法可以根据对象的存活特性,采取不同的回收策略,从而提高垃圾回收的效率。