内存堆栈分析笔记

2026 祝大家元旦快乐

内存泄漏

内存泄漏:程序未能释放已经不再使用的内存,导致内存占用持续增长,影响系统性能甚至导致进程崩溃。

关键点:内存无法被正常释放,如果是正常/合理的内存占用,或者后续能正常释放内存,不算内存泄漏。

常见的内存泄漏情况

  • 组件内未释放的定时器、事件监听、插件实例:组件销毁时没有清理定时器、事件监听器。
  • 意外全局变量 :全局声明,函数内赋值,或者 this 属性意外指向全局变量,在离开时没有解除引用。
  • 闭包函数:闭包本身不属于内存泄漏(合理内存占用),闭包意外引用而在后续想销毁无法销毁的情况下才算内存泄漏。
  • 创建后没有清理的 DOM 元素:DOM 元素被引用但未及时清理。
  • URL.createObjectURL() 未释放 :使用 URL.createObjectURL() 创建对象 Blob URL 后,需要调用 URL.revokeObjectURL() 释放内存。

内存泄漏(Memory Leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。

并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

内存分析手段

前端内存分析常用两种方式:Performance 性能分析面板和 Memory 堆内存分析。

Performance 面板分析

Performance 面板是常见的性能和内存分析手段,可以简单地确认是否存在内存泄漏情况,但不能作为主要依据。

流程:

  1. 打开 Performance 面板。
  2. 选择点击内存模块(图中 1)开启内存视图,执行一次垃圾回收(图中 2),避免其他影响。
  3. 运行性能检测(图中 3),执行可能存在的内存泄漏的操作(页面切换、组件挂载、弹窗、事件等)。
  4. 完成后,找到 JS Heap(图中 4):
    • 如果 JS Heap 曲线一直上升,则可能存在内存泄漏问题。
    • 如果 JS Heap 曲线有升有降,就是正常的垃圾回收操作。

注意:因为 V8 在初始加载过程中会不断对内存进行扩容,或者异步组件实例化时会占用多余的内存,只能确认可能存在内存泄漏。

堆快照分析

通常在上述 Performance 性能分析后,就可以定位到大致的内存泄漏进行解决。

但是有时候会难以确认具体的代码段,这时候就需要借助堆快照来完成。堆快照可以看到实际的内存占用和对象引用情况,可以更方便确认泄漏来源。

堆快照面板:

内存堆分析工具中的关键术语

  • Constructor(类名):对象的构造函数名称。
  • Distance(引用层级):对象到 GC 根节点的引用层级。
  • Shallow Size(自身大小):对象自身占用的内存大小。
  • Retained Size(保留大小):自身 + 引用内容大小(包含了外部引用对象)。
  • Object Count(对象数量):该类型对象的数量。
  • Retainers(引用关系):具体对象引用层级关系,用于追踪对象被哪些对象引用。

使用流程:

  1. 打开 Memory 面板。
  2. 执行一次垃圾回收,录制 Heap Snapshot(Snapshot A)。
  3. 执行可能存在内存泄漏的操作。
  4. 再执行一次垃圾回收,录制 Snapshot(Snapshot B)。
  5. 对比 Retained Size / Object Count ,如果 Object Count 持续增加,而且 Retained Size 无法释放,那该类对象可能存在内存泄漏情况。

代码段定位流程:

  1. 找到 Retained Size 中的最大实例:在内存堆快照中,找到 Retained Size 最大的对象实例。
  2. 在 Retainers 中找到 context in 定位源码进行优化 :通过 Retainers 面板查看对象的引用关系,找到 context in 标记,定位到源码位置进行优化。

这个方案可以快速确认内存来源,但不一定准确,更多的是确认可能存在内存占用过多的情况。

内存泄漏常见解决方案

定时器、监听事件泄漏、全局引用泄漏

在离开 Vue 组件时,beforeDestroy / beforeUnMount 钩子中:

  • 销毁所有监听器。
  • 销毁所有定时器。
  • 移除 DOM、全局对象、插件的引用:obj = null。如果是插件,执行插件内部的销毁函数。

闭包存在内存泄漏

根据闭包泄漏的可能情况来避免:

  • 闭包内部引用了全局变量:闭包持有全局变量的引用,导致变量无法被回收。
  • 引用了 DOM 节点:即事件监听的情况,引用了某个对象变量。
  • 定时器内部存在的闭包引用:即定时器本身的情况,闭包持有定时器的引用。
  • 闭包循环引用:闭包和对象之间形成循环引用,导致 GC 无法回收。

示例:闭包与对象循环引用

javascript 复制代码
// 示例:闭包与对象循环引用
function createCycle() {
  var obj = { name: "test" };
  var bigData = new Array(1000000).fill("cycle data");
  // obj.closure(闭包)引用了 obj,obj 又持有 closure 的引用,形成循环。GC 无法判断这组引用是否 "有用",会一直保留在内存中。
  obj.closure = function () {
    console.log(bigData.length, obj.name); // 闭包引用 bigData 和 obj
  };

  // 循环引用:obj 持有闭包,闭包持有 obj
  return obj;
}

var cycleObj = createCycle(); // cycleObj 持有 obj 引用
// 即使后续不再使用 cycleObj,循环引用导致 bigData 和 obj 无法回收

Chrome DevTools 工具

在打开 DevTools 时,console 对象会持有引用,可能存在泄漏。

javascript 复制代码
const obj = { large: new Array(1000000) };
console.log(obj);

这里 console 会保存 obj 的引用,方便我们后续展开对象查看属性,或者通过 $0$1 再次访问。

这个设计实际上不是内存泄漏,只是内存占用过多的场景,可能会影响后续内存分析,只要关闭 DevTools 再打开就能正常。

Map 和 Set

已知 Map 和 Set 会对存储的对象持有强引用,如果没有正确管理的话,会导致对象无法被正常释放,存在内存泄漏。

这类场景下,可以使用 WeakMap / WeakSet 进行优化。WeakMap 的 key / WeakSet 的值是弱引用对象,当对象没有被其他强引用持有的时候,会被在适当的时候被垃圾回收器回收,避免强引用导致的内存泄漏。

所以 WeakMap / WeakSet 中的对象数量是不可预测的,因此不会提供迭代和 size 长度查询这些接口。

总结

  • 内存泄漏核心概念:内存无法被正常释放,如果是正常/合理的内存占用,或者后续能正常释放内存,不算内存泄漏。
  • 内存分析手段
    • Performance 面板分析:通过 JS Heap 曲线判断是否存在内存泄漏,曲线一直上升可能存在泄漏,有升有降为正常垃圾回收。注意 V8 初始加载和异步组件实例化会占用多余内存,只能作为初步判断。
    • 堆快照分析:通过对比多个快照的 Retained Size 和 Object Count,定位具体泄漏来源。
    • 代码段定位流程
      • 找到 Retained Size 中的最大实例。
      • 在 Retainers 中找到 context in 定位源码进行优化。
  • 解决方案
    • 定时器、监听事件、全局引用泄漏 :在组件 beforeDestroy / beforeUnMount 钩子中销毁所有监听器、定时器,移除 DOM、全局对象、插件的引用。
    • 闭包内存泄漏:避免闭包内部引用全局变量、DOM 节点、定时器,避免闭包循环引用。
    • Map 和 Set 强引用:使用 WeakMap / WeakSet 替代,利用弱引用特性避免强引用导致的内存泄漏。
    • Chrome DevTools 影响:注意 console 对象会持有引用,可能影响内存分析,关闭 DevTools 再打开即可恢复正常。
相关推荐
前端小L2 小时前
贪心算法专题(十四):万流归宗——「合并区间」
javascript·算法·贪心算法
Geoffwo2 小时前
Electron 打包后 exe 对应的 asar 解压 / 打包完整流程
前端·javascript·electron
柒@宝儿姐2 小时前
vue3中使用element-plus的el-scrollbar实现自动滚动(横向/纵横滚动)
前端·javascript·vue.js
Geoffwo2 小时前
Electron打包的软件如何使用浏览器插件
前端·javascript·electron
LYOBOYI1232 小时前
qml练习:创建地图玩家并且实现人物移动(2)
开发语言·qt
电商API&Tina2 小时前
【电商API接口】多电商平台数据API接入方案(附带实例)
运维·开发语言·数据库·chrome·爬虫·python·jenkins
1001101_QIA2 小时前
【C++笔试题】递归判断数组是否是递增数组
开发语言·c++
zhangx1234_2 小时前
C语言 题目2
c语言·开发语言
YJlio2 小时前
网络与通信具总览(14.0):从 PsPing 到 TCPView / Whois 的联合作战
开发语言·网络·php