面试必考-3K字教会你JS中的垃圾回收机制。
写在前面
面试时老生常谈的话题之一莫过于JS中的垃圾回收机制,那么我们应该如何回答这个问题呢?小编认为有这样一个公式可以回答这个问题,就是 4W1H 原则啦,简单来说,就是 What(什么)、Where(哪里)、When(何时)、Why(为什么)和How(如何)。 好啦,我们开始步入正文吧。 当然如果你比较喜欢或者满意此次阅读的话,可以给笔者点个赞以做鼓励。
什么是垃圾(what)
在JavaScript中,"垃圾"是指不再被程序使用或引用的对象或变量。
垃圾在哪里(Where)
垃圾是在内存中。
何时触发垃圾回收呢(When)
通过前文,我们知道了垃圾是不在使用的变量或者对象, 那么在JS中我们需要怎么处理垃圾回收呢?答案是不需要处理。因为JS是具有自动垃圾收集,这种垃圾回收机制是会按照固定的时间间隔(或代码执行中预定的收集时间)周期性的执行这一操作。
为什么要进行垃圾回收(Why)
当对象或变量不再被程序需要时,它们占用内存空间,但无法通过程序访问或利用它们。这些无用的对象或变量被称为"垃圾",因为它们占用内存资源而没有任何实际用途。
垃圾回收的原理(How)
垃圾回收的原理是通过识别程序中不再被引用或使用的对象,并释放它们占用的内存空间,以便其他对象可以使用。垃圾回收器会定期扫描内存,找出哪些对象是垃圾,并将其回收。
在JavaScript中,主要使用两种垃圾回收算法: 标记清除和引用计数
标记清除
简单来说,就是当变量进入环境(例如,在函数中声明一个变量),我们就将这个变量标记为“进入环境”,从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要进入环境,我们就有可能会用到。当变量离开环境(函数执行结束,函数内部的变量),就可以标记为“离开环境”。 标记清除分为以上2个步骤,标记阶段(进入环境)和清除阶段(离开环境)
标记阶段 通过 一组根对象 (在浏览器环境里包括但不限于Window对象,dom对象)出发,通过引用关系去遍历出所有被引用到的对象,所有被遍历到的对象都会被打上标记,从而表示这个对象是 可达的
清除阶段 当变量或者对象离开环境,我们会去掉标记,会周期性的把这些没有标记的对象或者函数给清除,达到释放内存的效果。
标记清除的好处
- 简单易实现:标记清除算法相对简单,容易实现。
- 不会产生内存碎片:标记清除算法可以有效地回收不再使用的内存块,不会产生内存碎片。
- 可以处理循环引用:标记清除算法可以处理循环引用的情况,确保不再使用的对象可以被正确回收。
标记清除的缺点
- 垃圾回收时会产生停顿:标记清除算法在进行垃圾回收时,需要遍历所有对象进行标记,这个过程可能会导致应用程序的停顿。
- 内存泄漏:标记清除算法可能会出现内存泄漏的情况,即某些对象被错误地保留下来,无法被回收。
- 频繁的垃圾回收:标记清除算法可能会导致频繁的垃圾回收,影响应用程序的性能。
引用计数
简单来说,就是跟踪记录每一个值被引用的次数。当声明了一个变量并将一个引用类型的值复制给该变量时,这个值的引用次数就是1,如果同一个值又被复制给另一个变量,则该值的引用次数+1,如果减少引用,就-1.例如
let test1 = {name: 'lemon'}
let test2 = test1 //这里test1 计数为1
let test 3 = test1 // 这里test1 计数+1,变成2
test 3 = 5 // 这里test3不在使用test1的内存,所以test1的计数为2-1=1
test 2 = 5 // 这里test2不在使用test1的内存,所以test1的计数为1-1 = 0
引用计数的好处
- 及时回收垃圾:引用计数算法可以在对象不再被引用时立即回收这些对象,而不需要等待特定的垃圾回收周期。
- 简单实现:引用计数算法相对简单,易于实现。
引用计数的缺点
1.循环引用无法释放内存
let obj1 = {};
let obj2 = {};
obj1.child = obj2;
obj2.parent = obj1;
// 这里属于循环引用,这里都无法被清除。
- 性能开销大:引用计数算法需要维护对象的引用计数,每次引用发生变化时都需要更新计数,这会增加一定的性能开销。
- 频繁的垃圾回收:由于引用计数算法无法处理循环引用,可能导致频繁的垃圾回收,影响应用程序的性能。
V8垃圾回收机制(谷歌)
有读者会问,不是说js是自动处理垃圾的吗?为啥还有V8的事情?不急不急,听我慢慢道来。 虽然JavaScript具有垃圾自动回收功能,但是在实际执行过程中,对象的创建和销毁是非常频繁的,因此可能会产生大量的垃圾对象。这些垃圾对象占用内存空间,如果不及时回收,就会导致内存泄漏,进而影响程序的性能和稳定性。
什么是V8
V8作为Google开发的开源JavaScript引擎,主要用于执行JavaScript代码,V8引擎最初是为Google Chrome浏览器而开发的,V8引擎采用C++编写,具有高性能和优化的特点。它采用即时编译技术(JIT编译)将JavaScript代码直接编译成本地机器代码,而不是解释执行,从而提高了JavaScript代码的执行速度。V8引擎还包含了一套高效的垃圾回收机制,用于管理内存并回收不再使用的对象,防止内存泄漏。
V8垃圾回收机制
V8引擎的垃圾回收机制主要包括两个部分:分代垃圾回收和增量式垃圾回收。
分代垃圾回收
V8引擎将内存分为新生代和老生代部分。 新生代是存放声明周期短的对象 老生代是存放声明周期长的对象 如何区分声明周期长/短? 通过Scavenge算法来区分。Scavenge算法如下: 将新生代内存空间分为两个区域,分别是from和to空间。新创建的对象会分分配到from空间,当from空间达到一定数量时,v8会启动垃圾回收机制,将活动对象复制到to空间。同时清理掉没有被引用的对象。进过一段时间后,from和to空间会互换,这样就完成了一次新生代对象的垃圾回收。
老生代:老生代主要存放生命周期较长的对象,通常是经过多次垃圾回收仍然存活的对象。对于老生代对象的垃圾回收,V8引擎使用标记-清除算法和标记-整理算法。标记-清除算法首先会标记所有活动对象,然后清理掉未被标记的对象;标记-整理算法则会先标记所有活动对象,然后将它们整理到一端,清理掉未被标记的对象,从而减少内存碎片。
增量式垃圾回收
在传统的垃圾回收算法中,当进行全面的垃圾回收时,会暂停程序的执行,直到垃圾回收完成。这种暂停时间可能会导致程序的卡顿,影响用户体验。为了减少这种停顿时间,V8引擎引入了增量式垃圾回收的概念。 增量式垃圾回收的基本思想是,在执行JavaScript代码的同时,逐步进行垃圾回收,而不是等到所有代码执行完毕再进行一次全面的垃圾回收。V8引擎在执行代码的空闲时间,会执行一部分垃圾回收操作,然后继续执行代码,循环进行,直到完成整个垃圾回收过程。这样可以将垃圾回收的成本分摊到多个时间片段中,避免长时间的停顿。
总结
- js基本类型存储在栈内存中,栈内存时自动分配内存的机制,在作用域结束时会自动被销毁,不需要进行垃圾回收,我们这边说的垃圾回收是针对引用类型的对象或者变量。
- js会自动周期性处理不在使用的对象或者变量,大多是通过标记清除和引用计数的方式来处理的。
- V8引擎更好的处理了js垃圾回收问题。
思考
了解垃圾回收机制以后,如何优化代码呢?
1.局部变量和立即释放内存 使用局部变量而不是全局变量可以更快地释放内存。这是因为局部变量的生命周期通常比全局变量短,一旦离开了它的环境(例如:函数执行结束),局部变量就可以被标记为垃圾回收。
function test() { var local = "I'm a local variable";
// 当函数执行结束后,local 就离开了环境,可以被垃圾回收 } test();
2.解除对象引用
var obj = { prop: "I'm an object" }; obj = null; // 现在,obj 可以被垃圾回收
3.使用合适的数据结构 选择合适的数据结构可以减少内存占用和提高代码执行效率,比如使用Map代替对象等。
4.避免频繁创建和销毁对象:频繁创建和销毁对象会增加垃圾回收的负担,尽量复用对象或者使用对象池。
5.使用事件委托:使用事件委托可以减少事件处理函数的数量,提高性能。
6.避免不必要的DOM操作:减少DOM操作可以提高页面的加载和渲染速度,尽量批量操作DOM元素。
7.使用性能优化工具:可以使用性能优化工具来分析代码性能瓶颈,找出优化的方向。
转载自:https://juejin.cn/post/7366548334159429671