JS 垃圾回收 与 闭包先看一个问题: 以上代码中,bigArrayBuffer永远不会被 GC。我没想到会这样,因为:
先看一个问题:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
const id = setTimeout(() => {
console.log(bigArrayBuffer.byteLength);
}, 1000);
return () => clearTimeout(id);
}
globalThis.cancelDemo = demo();
以上代码中,bigArrayBuffer
永远不会被 GC。我没想到会这样,因为:
- 一秒钟后,引用
bigArrayBuffer
的函数不再可调用。 - 返回的取消函数不引用
bigArrayBuffer
。
但这并不重要。原因如下:
JavaScript引擎相当智能
下面这种情况不会泄露:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
console.log(bigArrayBuffer.byteLength);
}
demo();
函数执行后,bigArrayBuffer
不再需要,因此可以被GC。
这种情况也不会出现内存泄露:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
setTimeout(() => {
console.log(bigArrayBuffer.byteLength);
}, 1000);
}
demo();
在这种情况下:
- 引擎看到
bigArrayBuffer
被内部函数引用,所以它被保留下来。它与调用demo()
时创建的作用域相关联。 - 一秒钟后,引用
bigArrayBuffer
的函数不再可调用。 - 由于作用域内没有可调用的函数,作用域可以被 GC,
bigArrayBuffer
也可以被 GC。
下面这种情况,仍然不会出现内存泄漏:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
const id = setTimeout(() => {
console.log('hello');
}, 1000);
return () => clearTimeout(id);
}
globalThis.cancelDemo = demo();
在这种情况下,引擎知道不需要保留bigArrayBuffer
,因为内部可调用函数没有访问它。
问题案例
下面这种情况就是开始出现问题的场景:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
const id = setTimeout(() => {
console.log(bigArrayBuffer.byteLength);
}, 1000);
return () => clearTimeout(id);
}
globalThis.cancelDemo = demo();
在这种场景下,会出现内存泄露问题,因为:
- 引擎看到
bigArrayBuffer
被内部函数引用,所以它被保留下来。它与调用demo()
时创建的作用域相关联。 - 一秒钟后,引用
bigArrayBuffer
的函数不再可调用。 - 但是,作用域仍然存在,因为'取消'函数仍然可调用。
bigArrayBuffer
与作用域相关联,所以它仍然保留在内存中。
我以为引擎会更智能,并且由于bigArrayBuffer
不再可引用,所以可以进行 GC,但事实并非如此。
globalThis.cancelDemo = null;
现在 bigArrayBuffer
可以被 GC 了,因为作用域内没有可调用的函数。
这不仅仅与定时器有关,这只是我遇到问题的方式。例如:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
globalThis.innerFunc1 = () => {
console.log(bigArrayBuffer.byteLength);
};
globalThis.innerFunc2 = () => {
console.log('hello');
};
}
demo();
// bigArrayBuffer被保留,如预期。
globalThis.innerFunc1 = undefined;
// bigArrayBuffer仍然被保留,出乎意料。
globalThis.innerFunc2 = undefined;
// bigArrayBuffer现在可以被收集了。
学到新知识了!
更新
一个IIFE足以触发泄漏
我原本以为这种“捕获”值只发生在函数的初始执行之后仍然存在的函数中,但事实并非如此:
function demo() {
const bigArrayBuffer = new ArrayBuffer(100_000_000);
(() => {
console.log(bigArrayBuffer.byteLength);
})();
globalThis.innerFunc = () => {
console.log('hello');
};
}
demo();
// bigArrayBuffer被保留,出乎意料。
在这里,内部的IIFE足以触发内存泄漏。
这是一个跨浏览器的问题
整个问题在各个浏览器中都存在,并且由于性能问题不太可能被修复。
我不是第一个写这个问题的人
- 为什么会出现这种情况的底层解释,由Slava Egorov在2012年撰写
- Meteor工程师发现它,由David Glasser在2013年撰写
- 这个以React为中心的解释,由Kevin Schiener在2024年5月最近撰写。
不,这不是因为eval()
Hacker News和Twitter上的人们很快指出这是因为eval()
,但事实并非如此。
eval
很棘手,因为它意味着代码可以在无法静态分析的作用域内存在:
function demo() {
const bigArrayBuffer1 = new ArrayBuffer(100_000_000);
const bigArrayBuffer2 = new ArrayBuffer(100_000_000);
globalThis.innerFunc = () => {
eval(whatever);
};
}
demo();
innerFunc
中是否访问了缓冲区?无法知道。但浏览器可以确定eval
存在。这导致了一个去优化,其中父作用域中的所有内容都被保留。
浏览器可以确定这一点,因为eval
有点像一个关键字。在这种情况下:
const customEval = eval;
function demo() {
const bigArrayBuffer1 = new ArrayBuffer(100_000_000);
const bigArrayBuffer2 = new ArrayBuffer(100_000_000);
globalThis.innerFunc = () => {
customEval(whatever);
};
}
demo();
......
去优化不会发生。因为eval
关键字没有直接使用,whatever
将在全局作用域内执行,而不是在innerFunc
内。这被称为“间接eval”,MDN上有更多关于这个话题的信息。
这种行为的存在,特别是为了让浏览器能够将这种去优化限制在可以静态分析的案例中。
转载自:https://juejin.cn/post/7423644190314283035