js垃圾回收机制
在js中创建一个变量时,会自动分配内存空间,当变量不再被使用时,垃圾回收机制会自动释放相应的内存空间。
如何判断一个变量不在被使用?方法有两种:
一、引用计数法:
引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,若是没有任何对象再指向它,那么垃圾回收器就会回收,其策略是跟踪记录每个变量值被使用的次数
-
当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为 1
-
如果同一个值又被赋给另一个变量,那么引用数加 1
-
如果该变量的值被其他的值覆盖了,则引用次数减 1
-
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
引用计数存存在一个致命的缺陷,当对象间存在循环引用时,引用次数始终不会为0,因此垃圾回收器不会释放它们。
1 2 3 4 5 6 7 | function f() { var o1 = {}; var o2 = {}; o1.a = o2; // o1 引用 o2 o2.a = o1; // o2 引用 o1 return ; }; |
1 2 3 4 | var element = document.getElementById( "some_element" ); var myObject = new Object{); myObject. element = element; element.someObject = myObject; |
1 | 这个例子在一个DOM元素(element)与一个原生 JavaScript对象(myobject)之间创建了循环引用。而想要解决循环引用,需要将引用地址置为 null 来切断变量与之前引用值的关系,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。 |
1 2 | myObject.element = null ; element,SomeObject = null ; |
二、标记清除法:
标记清除(Mark-Sweep),目前在 JavaScript引擎
里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎
都在采用标记清除算法。
此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。
标记阶段,垃圾回收器会从根对象开始遍历(在js中,通常认定全局对象window做为根)。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。
清除阶段,垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。
在标记阶段,从根对象1可以访问到B,从B又可以访问到E,那么B和E都是可到达对象,同样的道理,F、G、J和K都是可到达对象。
在回收阶段,所有未标记为可到达的对象都会被垃圾回收器回收。
标记清除法会导致内存碎片化。由于空闲内存块是不连续的,容易出现很多空闲内存块,假设我们新建对象分配内存时需要大小为 size
,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size
的块才能为其分配(如下图)
那如何找到合适的块呢?我们可以采取下面三种分配策略
-
First-fit
,找到大于等于size
的块就立即返回 -
Best-fit
,遍历整个空闲列表,返回大于等于size
的最小分块 -
Worst-fit
,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size
大小,并将该部分返回
这三种策略里面 Worst-fit
的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit
和 Best-fit
来说,考虑到分配的速度和效率 First-fit
是更为明智的选择,但即便是使用 First-fit
策略,其操作仍是一个 O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
三、v8引擎的垃圾回收机制
Chrome 浏览器所使用的 V8 引擎采用的分代回收策略,该策略通过区分「临时」与「持久」对象;多回收「临时对象区」,少回收「持久对象区」,减少每次需遍历的对象,从而减少每次GC的耗时。
「临时」与「持久」对象也被叫做作「新生代」与「老生代」对象。
1. 新生代的特点:
- 通常把小的对象分配到新生代
- 新生代的垃圾回收比较频繁
- 通常存储容量在1~8M
2. 新生代-Scavenge算法
该算法将新生代分为两部分,一部分叫做from(对象区域),另一部分叫做to(空闲区域),新加入的对象首先存放在from区域;
from区域写满的时候,对from区域开始进行垃圾回收。首先对from区域的垃圾进行标记(红色代表标记为垃圾);
将存活的对象复制到to区域中,并且有序地排列起来,复制后的to区域就没有内存碎片了;
清空from区域;
from区域和to区域进行反转,也就是原来的from区域变为to区域,原来的to区域变成from区域。
Scavenge算法在时间效率上有着优异的表现,缺点是只能使用堆内存中的一半,如果存储容量过大,就会导致每次清理的时间过长,效率低,因此经过两次垃圾回收之后依然存活的对象会晋升为老生代对象,另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge
回收后,空闲区将翻转成对象区域,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。
3. 老生代的特点:
- 对象占用空间大
- 对象存活时间长
4. 老生代-标记整理法
- 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象;
- 整理:让所有存活的对象都向内存的一端移动
5. 何时执行垃圾回收?
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法
四、哪些情况容易引起内存泄漏
JS 内存泄露通常是由于对象被错误地持有,导致垃圾回收(GC)无法释放它们。以下是常见的内存泄露情况:
1. 挂载了太多的全局变量
全局变量等同于在window上添加属性,因此在函数执行完毕,依旧能够访问到它,因此不能够被回收。
2. 闭包
闭包可以在外部引用内部的变量,可能会导致外部作用域的变量无法释放
1 2 3 4 5 6 7 | function leakyClosure() { let bigData = new Array(1000000).fill( "leak" ); return function () { console.log(bigData.length); }; } let leaky = leakyClosure(); // bigData 无法被回收 |
显式设置 bigData = null
解除引用
3. DOM 引用未清理
如果 JS 代码持有对已删除的 DOM 元素的引用,内存无法回收:
1 2 3 4 | let div = document.createElement( "div" ); document.body.appendChild(div); let divRef = div; document.body.removeChild(div); // DOM 删除了,但 divRef 还引用着 |
显式设置divRef = null解除引用。
4. 定时器(setInterval / setTimeout)未清理
定时器中引用了外部变量,但未手动 clearInterval
,导致变量无法回收:
1 2 3 4 5 6 7 | function startTimer() { let data = new Array(1000000).fill( "leak" ); setInterval(() => { console.log(data.length); }, 1000); } startTimer(); // data 永远不会被释放 |
解决:在适当的时机 clearInterval()
5.事件监听器未移除
如果给 DOM 绑定了事件监听器,但在删除元素时未移除,可能会导致泄露:
1 2 3 4 5 | let btn = document.getElementById( "myButton" ); btn.addEventListener( "click" , function () { console.log( "Clicked!" ); }); document.body.removeChild(btn); // btn 被移除,但监听器仍然存在 |
6. Map 和Set的泄露
值使用了引用类型的变量,变量设置为null,但map实例还存在
1 2 3 4 | let map = new Map(); let obj = { key: "value" }; map.set(obj, "some data" ); obj = null ; // 但 map 仍然持有 obj 的引用 |
解决方案:使用 WeakMap
:
1 2 3 4 | let weakMap = new WeakMap(); let obj = { key: "value" }; weakMap.set(obj, "some data" ); obj = null ; // WeakMap 不会阻止垃圾回收 |
五、如何分析内存泄露
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了