likes
comments
collection
share

JS 垃圾回收 与 闭包先看一个问题: 以上代码中,bigArrayBuffer永远不会被 GC。我没想到会这样,因为:

作者站长头像
站长
· 阅读数 20

先看一个问题:

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();

在这种情况下:

  1. 引擎看到bigArrayBuffer被内部函数引用,所以它被保留下来。它与调用demo()时创建的作用域相关联。
  2. 一秒钟后,引用bigArrayBuffer的函数不再可调用。
  3. 由于作用域内没有可调用的函数,作用域可以被 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();

在这种场景下,会出现内存泄露问题,因为:

  1. 引擎看到bigArrayBuffer被内部函数引用,所以它被保留下来。它与调用demo()时创建的作用域相关联。
  2. 一秒钟后,引用bigArrayBuffer的函数不再可调用。
  3. 但是,作用域仍然存在,因为'取消'函数仍然可调用。
  4. 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足以触发内存泄漏。

这是一个跨浏览器的问题

整个问题在各个浏览器中都存在,并且由于性能问题不太可能被修复。

我不是第一个写这个问题的人

不,这不是因为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
评论
请登录