JavaScript 如何使用垃圾回收机制来自动管理内存

当JavaScript程序创建对象和变量时,它们会被分配到内存中。当对象和变量不再被引用时,它们就可以被认为是不需要的,就会被垃圾回收器回收以释放内存。

自动内存管理

无论您使用哪种编程语言,内存生命周期几乎总是相同的:分配内存-使用内存-释放内存。

C 语言是手动内存管理的语言,程序员需要手动分配和释放内存,程序员使用 malloc() 函数动态分配内存并使用 free() 函数释放内存,这可以使程序员获得更多的控制权和更高的性能,但也容易出现内存泄漏和野指针等问题。

而 PHP 和 JavaScript 语言是自动内存管理的语言,拉圾回收器会自动释放不需要的内存。

PHP 早期使用引用计数算法来识别不再使用的对象,并回收它们的内存。PHP 5.3版本之后,引入标记清除和标记整理的垃圾回收机制。PHP 7版本又引入了一个新的内存管理器 Zend Memory Manager,它的目标是在性能和内存使用之间取得更好的平衡。

早期的 JavaScript 也是引用计数算法来回收内存。然而,引用计数算法有一个缺陷,就是无法处理循环引用的情况,现代的 JavaScript 使用标记清除和标记整理算法等。

引用计数(Reference-counting garbage collection)

引用计数记录每个对象被引用的次数,如果某个对象有 0 个指向它的引用,则该对象被称为垃圾,会被回收。

let person = {
  name: 'John',
  age: 22,
  hobbies: ['hiking', 'reading']
}
let newPerson = person // newPerson 变量也引用了person 指向的对象
let hobbies = newPerson.hobbies // hobbies 变量引用了 person 指向的对象的hobbies 属性
person = null // 最初 person 指向的对象只被newPerson 引用了
newPerson = null // 最初 person 指向的对象引用为零,它可以被回收,他它指向的hobbies 属性仍被 hobbies 引用,这部分不能被回收

但对于循环引用(Circular References)这种情况,引用计数不为0,那么就无法根据计数为 0 的原则来回收拉圾了,这就会导致内存泄漏。因此,在现代 JavaScript 引擎中,已经不再使用引用计数了。

function foo() {
  var obj1 = {}
  var obj2 = {}
  obj1.prop = obj2
  obj2.prop = obj1
  return "Hello World!"
}

上面的obj1 和 obj2 互相引用,形成循环,因为引用计数不为 0, 所以无法被回收而导致内存泄漏。

标记清除(Mark-and-sweep )

垃圾回收器会定期从全局对象 Root (浏览器中是 window 对象,NodeJS 中是 global 对象)开始,遍历所有引用对象,看是否能从 Root 对象访问,能则标记为活动(Active)对象,然后清除所有未标记的对象。

这种方法可以有效地回收内存,包括循环引用的,自2012 年以来,该算法已在所有现代浏览器中实现。但可能会导致性能问题,因为需要遍历整个对象图。

标记清除和整理(Mark and Sweep with Compacting)

在标记清除算法中,当内存中的垃圾对象被回收后,会产生空洞,导致内存碎片的产生,从而影响了内存的分配效率。而标记整理算法则通过在垃圾回收过程中将存活对象整理到一端,将空间空出来,从而减少了内存碎片的产生。

一些无法被垃圾回收器回收的值

全局变量不会被回收

全局变量会一直存活在内存中,除非网页被关闭。所以要避免不必要的全局变量。

定时器和事件监听器所引用的函数和变量

定时器和事件监听器:在 JavaScript 中,如果使用 setInterval()addEventListener() 等方法注册了定时器或事件监听器,那么它们所引用的函数和变量将无法被垃圾回收器回收,直到定时器或事件被清除。

当我们在控制台中解绑这 5 个监听,并生成快照2 。最终发现,在快照2中已找不到这5个闭包了,所以按需解绑监听,也能避免内存泄漏。

闭包

查看《闭包引起的内存泄漏》