JavaScript 内存泄漏

什么是内存泄漏?

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

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

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

所有高级语言都有自己的内存管理系统,而 js 的内存管理也包含在 v8 中。提供自动内存管理,称为”垃圾回收机制”

垃圾回收机制

垃圾回收机制怎么知道,哪些内存不再需要呢?

引用计数:
语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是 0,就表示这个值不再用到了,因此可以将这块内存释放

那么,一个值不再需要了,引用数却不为 0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

示例:

1
2
3
4
5
6
7
// 地址 arr, 地址: 数组值. 此时 数组值的<引用次数>是 1
let arr = [1, 2, 3, 4];
console.log("hello world");
// 解除对数组值的引用
arr = null;
// 或者可以换位其他值, 总之就是不用这个数组了,那声明的数组值会被清理掉
arr = undefined;

内存泄漏的识别方法

如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用

  1. 打开开发者工具,选择 性能(performance) 面板
  2. 在顶部的 栏目里面勾选 Memory
  3. 点击左上角的录制按钮。
  4. 在页面上进行各种操作,模拟用户的使用情况。
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。

内存工具

结果判定:

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏

如果随着你的操作,内存线不断升高,那么就要小心了

Nodejs 查看当前内存
process.memoryUsage()

1
2
3
4
5
6
7
8
/**
{
rss(resident set size):所有内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。
}
*/

判断内存泄漏,以 heapUsed 字段为准

WeakMap

在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。

可以用 WeakSetWeakMap 实现,它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个”Weak”,表示这是弱引用

如何使用 WeakMap:

1
2
3
4
5
6
7
const wm = new WeakMap();
// 此时 dom 节点的 引用数为 1
const element = document.getElementById("example");
// 此时 dom 节点的引用数仍是 1. 如果是 <Map> 而不是 <WeakMap> 那么 dom 节点的引用数是 2
wm.set(element, "some information");
wm.get(element); // "some information"
// 浏览器内无法观察到被回收

WeakMap 显示内存示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 首先,打开 Node 命令行。--expose-gc参数表示允许手动执行垃圾回收机制
node --expose-gc

# 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined

# 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4339536,
external: 9059 }

> let wm = new WeakMap();
undefined

> let b = new Object();
undefined

> global.gc();
undefined

# 此时,heapUsed 仍然为 4M 左右
> process.memoryUsage();
{ rss: 20537344,
heapTotal: 9474048,
heapUsed: 4620568,
external: 8993 }

# 在 WeakMap 中添加一个键值对,
# 键名为对象 b,键值为一个 5*1024*1024 的数组
> wm.set(b, new Array(5*1024*1024));
WeakMap {}

# 手动执行一次垃圾回收
> global.gc();
undefined

# 此时,heapUsed 为 46M 左右
> process.memoryUsage();
{ rss: 62652416,
heapTotal: 51437568,
heapUsed: 46608096,
external: 8951 }

# 解除对象 b 的引用
> b = null;
null

# 再次执行垃圾回收
> global.gc();
undefined

# 解除 b 的引用以后,heapUsed 变回 4M 左右
# 说明 WeakMap 中的那个长度为 5*1024*1024 的数组被销毁了
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 4529984,
external: 8956 }

上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。
如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap

也就是 WeakMapWeakSet 对 引用值有关联性
而且 WeakSet 的 value 值,WeakMap 的 key, 只能是对象

作者

Huasun47

发布于

2021-05-17

更新于

2021-05-17

许可协议