【硬核通俗理解】JavaScript 垃圾回收,闭包和内存泄漏!

273 阅读4分钟

大家好,我是前端架构师,关注微信公众号【程序员大卫】免费领取前端精品资料。

背景

在 JavaScript 开发中,合理的内存管理至关重要。由于 JavaScript 具有自动垃圾回收机制,开发者通常无需手动管理内存。但如果代码中存在不必要的引用或未能正确释放内存,仍然可能导致 内存泄漏,影响程序性能,甚至引发崩溃。

本篇文章介绍了 JavaScript 的 垃圾回收机制,分析了 内存泄漏的常见原因,并通过代码示例和 Chrome DevTools 的 Memory 面板,展示如何 检测和优化 内存使用,帮助开发者编写更加高效、稳定的代码。

1. 什么是垃圾

在程序运行过程中,如果某些内存数据不再被访问(即无法触达),这些数据就被称为垃圾。

2. 什么是内存泄漏

当满足以下两种情况时,就会发生内存泄漏:

  • 我们确定不再需要某块内存。
  • 垃圾回收器未能回收这块内存,导致它仍然占用空间。

3. 垃圾回收机制

JavaScript 采用 标记清除(Mark-and-Sweep)算法进行垃圾回收。

核心思想是:通过判断程序中哪些内存仍然可访问(即可触达),将无法访问的内存标记为垃圾,并将其清除。

4. 内存泄漏与闭包

  • 闭包本身不会导致内存泄漏,但如果闭包持有不再需要的函数引用,则会导致相关的词法环境无法销毁,从而造成内存泄漏。
  • 当多个函数共享同一个词法环境时,该环境可能无法销毁,使相关的内存无法被回收,最终引发内存泄漏。(示例见 5.3)

5. 示例

5.1 使用 FinalizationRegistry 监听垃圾回收

以下代码在几秒钟后,控制台会打印 清理 o 对象

let o: any = {};
setTimeout(() => {
  o = null;
}, 0);
const registry = new window.FinalizationRegistry((key) => {
  console.log("清理", key);
});
registry.register(o, "o 对象");

如果希望立即看到日志输出,可以切换到 Performance 面板,并点击 垃圾回收(Garbage Collection) 按钮。

5.2 WeakSet 与垃圾回收

const ws = new WeakSet();
let o: any = {};
ws.add(o);

setTimeout(() => {
  o = null;
}, 0);

console.log(ws.has(o)); // 垃圾尚未回收,返回 true

setTimeout(() => {
  console.log(ws.has(o)); // 垃圾已回收,返回 false
}, 0);

const registry = new window.FinalizationRegistry((key) => {
  console.log("清理", key);
});
registry.register(o, "o 对象");

5.3 特殊情况下的内存无法回收

通常情况下,无法触达的内存会被回收,但在某些特殊情况下可能不会。

例如,下面的代码在点击按钮后会创建 doms 数组,按理来说它无法被访问,应该被回收。然而,由于 _temp 函数引用了 doms,导致其占用的内存无法回收。

如果移除 _temp 函数,不管点击多少次按钮,doms 数组都会被正确回收。

<!DOCTYPE html>
<html>
  <head>
    <title>垃圾回收</title>
  </head>
  <body>
    <button id="btn">按钮</button>
    <script>
      function createIncreate() {
        const doms = new Array(100000).fill(0).map((_, i) => {
          const dom = document.createElement("div");
          dom.innerHTML = i;
          return dom;
        });
        function increate() {}
        function _temp() {
          doms;
        }
        return increate;
      }
      let increate;
      btn.addEventListener("click", () => {
        increate = createIncreate();
      });
    </script>
  </body>
</html>

5.4 使用 Memory 面板分析内存泄漏

可以使用 Memory 面板检查内存情况:

1.在右侧选择 Heap Snapshot

2.点击红色按钮 Take Heap Snapshot

第一次快照显示内存占用 11.7M

点击按钮后,再次点击 Take Heap Snapshot,生成第二个快照,内存占用增加至 31.3M

切换到 Comparison 标签,对比两个快照。

关键字段解释:

  • #New:表示在两个快照之间新创建的对象数量。
  • #Deleted:表示在两个快照之间被删除的对象数量。
  • #Delta:表示两个堆快照(heap snapshots)之间的对象数量变化。 计算公式:#Delta = #New - #Deleted
  • Alloc. Size:表示新分配的内存大小(单位:字节)。

总结

文章介绍了 JavaScript 的垃圾回收机制和内存泄漏问题。垃圾指无法访问的内存数据,而内存泄漏则是本应回收的内存未被释放。JavaScript 主要采用 标记清除(Mark-and-Sweep)算法进行垃圾回收。

关键点:

  • 闭包 可能导致不必要的内存占用,影响回收。
  • WeakSet 与 FinalizationRegistry 可用于优化内存管理。
  • 特殊引用情况 可能阻止垃圾回收,如函数保留不必要的变量。
  • Memory 面板 提供 Heap SnapshotComparison 视图,帮助分析内存变化,查找泄漏点。

本文通过示例代码和 Chrome DevTools 的调试方法,帮助开发者理解和优化 JavaScript 内存管理。

如果本文对你有帮助,欢迎点赞❤️收藏⭐,也欢迎在评论区交流!