当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()
等方法注册了定时器或事件监听器,那么它们所引用的函数和变量将无法被垃圾回收器回收,直到定时器或事件被清除。