引言
JavaScript的运行环境极大地依赖于垃圾回收(GC)机制,而V8引擎则是其中最著名的执行环境之一。V8引擎的垃圾回收机制保证了内存的有效管理,使得开发者可以在不直接处理内存分配和回收的情况下创建复杂的Web应用。我们来理解V8引擎的垃圾回收机制,包括栈回收、堆回收以及代际假说的应用。
基础
垃圾回收的基本思路是:查找内存中的所有对象,看哪些已经不再需要,然后释放这些对象所占用的内存。在V8中,JavaScript的内存空间分为栈(Stack)和堆(Heap)两部分。栈用于存储原始类型(如Number,String,Boolean,Null,Undefined,Symbol)
和引用对象
的内存地址,而堆用于存储引用类型
的对象。
栈回收
在V8引擎中,函数调用的参数、返回地址和局部变量都存储在调用栈中。每当一个函数被调用时,都会创建一个新的栈帧,其中包含这些信息。而栈帧的回收则非常直接:一旦函数调用结束,其栈帧就会被立即移除。这种机制依赖于ESP(Extended Stack Pointer)指针,该指针始终指向栈的顶部,用于追踪哪些栈帧是活动的,哪些可以被安全回收。
function getName(){ let a = 1 let b = { name: 'Hello World' } function foo() { let c = 2 let d = { name: 'Hello Javascript' } } foo() } getName()
堆回收
与栈回收机制相比,堆回收要复杂得多。在V8引擎中,所有的对象实例都存储在堆中。这些对象的生命周期不像栈帧那样简单明了,因此需要更复杂的机制来确定何时可以安全回收这些对象的内存。V8引擎主要采用标记-清除(Mark-Sweep)算法进行垃圾回收。该算法分为两个步骤:标记阶段,V8引擎会遍历所有的对象,标记活动对象和非活动对象;清除阶段,非活动对象所占用的内存将被回收。
function createObject() { let obj = new Object(); obj.value = 'Hello, World!'; return obj; } let myObject = createObject(); // 创建对象,分配内存 myObject = null; // 丢弃对象,垃圾回收器现在可以回收这个对象的内存
代际假说
为了提高垃圾回收的效率,V8引擎引入了代际假说,即“大部分对象在内存中存在的时间很短”,“老的对象会引用其他老的对象,新的对象会引用新的或老的对象”。基于这个假设,V8引擎把堆分成新生代和老生代两部分。新生代存放生命周期短的对象,使用Scavenge算法进行垃圾回收,此算法速度快,但是只能回收一半的空间。老生代存放生命周期长的对象,使用Mark-Sweep和Mark-Compact算法进行垃圾回收,虽然速度慢,但是可以回收全部的空间。当新生代中的对象经历了一定次数的垃圾回收仍然存活时,这些对象会被移动到老生代中。
function createObjects() { let objects = []; for(let i=0; i<100; i++) { objects.push(new Object()); // 创建大量对象,填满新生代空间 } return objects; } let persistentObjects = createObjects(); // 将部分对象保留在内存中,使其在垃圾回收后仍然存活,最终被移动到老生代
新生代垃圾回收策略
新生代和老生代由于存放的对象的生命周期和数量的差异,采取了不同的垃圾回收策略。
新生代主要存放生命周期短的对象,采用Scavenge算法进行垃圾回收。Scavenge算法将新生代划分为两个等大的空间,使用空间(From Space)和空闲空间(To Space)。新对象首先会被分配到使用空间,当进行垃圾回收时,会检查使用空间中的存活对象并复制到空闲空间,非存活对象直接舍弃,然后交换使用空间和空闲空间,重复此过程。这个过程的优点是执行快速,缺点是只能使用一半的空间。
老生代垃圾回收策略
老生代主要存放生命周期长或经过多次复制依然存活的对象。老生代采用了标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)两种算法。标记-清除算法在标记阶段遍历所有对象并标记活动对象,在清除阶段回收非活动对象的空间。标记-整理算法在标记-清除的基础上,会在清除阶段进行内存整理,移动对象位置让他们在内存中连续排列,这样可以解决内存碎片化的问题。
垃圾回收优化策略
- 延迟回收:由于垃圾回收会带来运行阻塞,因此可以选择在CPU空闲时或系统资源充足时执行垃圾回收,从而尽可能减少对应用程序运行的影响。
- 增量标记:由于全堆垃圾回收会导致JS应用暂停执行,为了减少全堆垃圾回收带来的卡顿,V8采用增量标记的策略。也就是将一次完整的垃圾回收分解为多个小的步骤,同时让垃圾回收和应用逻辑交替执行,以达到流畅的用户体验。
- 对象晋升:在新生代中存活下来的对象会被移动到老生代中,这就是对象晋升策略。在V8中通常采用两次垃圾回收后仍然存活的对象会被晋升到老生代。
- 空间预留:在执行垃圾回收时,会预留一部分空间,避免因空间不足导致频繁的垃圾回收。
通过上述策略,V8引擎不仅能高效地回收垃圾,而且能最大程度地减少垃圾回收对程序运行的干扰,实现了内存管理的效率与效果的平衡。
增量标记
当进行大规模的垃圾回收时,V8引擎使用增量标记来减少对应用程序的阻塞。
增量标记是一种垃圾回收的优化策略,它将一次完整的垃圾回收过程分解为多个小的步骤,使得垃圾回收和应用程序的逻辑可以交替执行。这样可以减少垃圾回收造成的长时间阻塞,提高应用程序的响应性和用户体验。
V8引擎的增量标记策略主要包括以下步骤:
- 初始标记(Initial Marking):在这个阶段,V8会标记出根对象和直接从根对象可达的对象,确定它们为活动对象。这个阶段需要阻塞应用程序的执行,但是尽量保持时间短暂。
- 并发标记(Concurrent Marking):在初始标记之后,V8引擎会启动增量标记线程,与应用程序的执行并发进行。增量标记线程会遍历剩余的对象图,标记出所有的活动对象。同时,应用程序的逻辑也在继续执行。
- 再标记(Remark):在并发标记过程中,应用程序可能会继续修改对象的引用关系,因此需要进行再标记。再标记阶段会对并发标记过程中发生变化的对象进行重新标记,以确保准确性。
- 清除阶段(Sweeping):在增量标记完成后,V8引擎会进行清除阶段,回收非活动对象所占用的内存。这个阶段通常会阻塞应用程序的执行,因为它需要遍历堆中的所有对象。
通过增量标记的方式,V8引擎可以在垃圾回收过程中与应用程序的逻辑交替执行,减少长时间的阻塞。这种方式可以有效降低垃圾回收对应用程序性能的影响,提高应用程序的响应速度和用户体验。
如何避免内存泄漏
在我们编写代码的过程中,尽管浏览器和大部分的前端框架已经帮助我们处理了常见的内存泄漏问题,但我们仍然有必要了解一些常见的内存泄漏问题以及避免它们的方式。以下是几种常见的避免内存泄漏的方式:
尽可能减少全局变量的使用
在 JavaScript 中,全局变量会一直存在于内存中,直到应用程序退出。因此,过多的全局变量会导致内存占用增加。为了避免这个问题,尽量减少全局变量的使用,尽可能将变量限定在局部作用域中。如果确实需要使用全局变量,确保在使用完毕后将其设置为 null
,以便垃圾回收机制可以及时释放内存。
手动清除定时器
在使用定时器时,一定要记得在适当的时机手动清除定时器。如果忘记清除定时器,定时器的回调函数将持续执行,可能导致内存泄漏。确保在不需要定时器时,使用 clearTimeout
或 clearInterval
主动清除定时器。
let timer = setInterval(() => { // 执行一些操作 }, 1000); // 当不再需要定时器时,手动清除 clearInterval(timer);
避免不必要的闭包
闭包是 JavaScript 的一个强大特性,但如果不小心使用,可能会导致内存泄漏。当闭包中引用了外部函数的变量时,即使外部函数执行完毕,被引用的变量也不会被垃圾回收,直到闭包不再被引用。因此,避免创建不必要的闭包或确保在不再需要时解除对闭包的引用,以便垃圾回收机制可以释放相关的内存。
function createClosure() { let data = "Sensitive Data"; return function() { // 闭包中引用了外部函数的 data 变量 console.log(data); }; } let closure = createClosure(); // 当不再需要闭包时,解除对闭包的引用 closure = null;
清除 DOM 引用
当操作 DOM 元素时,确保在不再需要使用它们时清除对 DOM 元素的引用。如果仍然保留对已移除或隐藏的 DOM 元素的引用,这些元素将无法被垃圾回收。
let element = document.getElementById("myElement"); // 当不再需要该 DOM 元素时,手动清除引用 element = null;
使用弱引用
在 ES6 中,引入了 WeakMap
和 WeakSet
这两个数据结构,它们可以帮助我们避免内存泄漏。这些数据结构使用弱引用,当对象没有其他引用时,垃圾回收机制会自动释放它们占用的内存。使用 WeakMap
和 WeakSet
可以减少手动清除引用的工作量。
let weakMap = new WeakMap(); let key = {}; // 使用弱引用方式设置键值对 weakMap.set(key, "value"); // 当不再需要 key 对象时,它会被自动回收,WeakMap 中的键值对也会被清除 key = null;