js垃圾回收机制
在js中创建一个变量时,会自动分配内存空间,当变量不再被使用时,垃圾回收机制会自动释放相应的内存空间。
如何判断一个变量不在被使用?方法有两种:
一、引用计数法:
引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,若是没有任何对象再指向它,那么垃圾回收器就会回收,其策略是跟踪记录每个变量值被使用的次数
- 
当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为 1
 - 
如果同一个值又被赋给另一个变量,那么引用数加 1
 - 
如果该变量的值被其他的值覆盖了,则引用次数减 1
 - 
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
 
引用计数存存在一个致命的缺陷,当对象间存在循环引用时,引用次数始终不会为0,因此垃圾回收器不会释放它们。
function f() {
    var o1 = {};
    var o2 = {};
    o1.a = o2; // o1 引用 o2
    o2.a = o1; // o2 引用 o1
    return;
};
var element = document.getElementById("some_element");
var myObject = new Object{);
myObject. element = element;
element.someObject = myObject;
这个例子在一个DOM元素(element)与一个原生 JavaScript对象(myobject)之间创建了循环引用。而想要解决循环引用,需要将引用地址置为 null 来切断变量与之前引用值的关系,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
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. 闭包
闭包可以在外部引用内部的变量,可能会导致外部作用域的变量无法释放
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 元素的引用,内存无法回收:
let div = document.createElement("div");
document.body.appendChild(div);
let divRef = div; 
document.body.removeChild(div); // DOM 删除了,但 divRef 还引用着
显式设置divRef = null解除引用。
4. 定时器(setInterval / setTimeout)未清理
定时器中引用了外部变量,但未手动 clearInterval,导致变量无法回收:
function startTimer() {
    let data = new Array(1000000).fill("leak");
    setInterval(() => {
        console.log(data.length);
    }, 1000);
}
startTimer(); // data 永远不会被释放
解决:在适当的时机 clearInterval()
5.事件监听器未移除
如果给 DOM 绑定了事件监听器,但在删除元素时未移除,可能会导致泄露:
let btn = document.getElementById("myButton");
btn.addEventListener("click", function () {
    console.log("Clicked!");
});
document.body.removeChild(btn); // btn 被移除,但监听器仍然存在
6. Map 和Set的泄露
值使用了引用类型的变量,变量设置为null,但map实例还存在
let map = new Map();
let obj = { key: "value" };
map.set(obj, "some data");
obj = null;  // 但 map 仍然持有 obj 的引用
解决方案:使用 WeakMap:
let weakMap = new WeakMap();
let obj = { key: "value" };
weakMap.set(obj, "some data");
obj = null; // WeakMap 不会阻止垃圾回收
五、如何分析内存泄露

                    
                
                
            
        
浙公网安备 33010602011771号