前言
上文我们聊了下V8的内存管理,内容偏原理解析性质,与平时写代码的关联性没有太明显的关系,那么本文就来聊聊与内存相关的关联性大的内容 ------ 内存泄漏。
常见的内存泄漏
所谓的内存泄漏,其实就如字面意思,就是当不再用到的对象内存没有及时被回收时,就是内存泄漏。
那么我们常见的几种内存泄漏都有哪些呢?
在聊这个之前,先说明下,这里使用来确定是否有内存泄漏的方式是通过浏览器的devtool中的内存模块,如下图。
通过这个模块,先进行手动的GC(手动垃圾回收)再执行快照,如果在内存中还能找到相关数据,那就证明产生了内存泄漏。
除此之外,也能直接通过调试中的closure确定是否有闭包。
不合理的闭包
闭包:一个函数和对其词法环境的引用捆包在一起的组合。也就是说一个内层函数中访问到外层函数的作用于,在js中,每当创建一个函数,闭包就会这函数被创建的同时而创建出来。
例子如下:
function Person() {};
function fn() {
const name = 'Rippi';
const age = 10;
const arr = new Array();
for (let i = 0; i < 1000; i++) {
arr.push(new Person());
}
return function() {
console.log(arr, name);
}
}
const persons = fn();
persons();
fn返回的匿名函数使用了name和arr,我们在图中可以看到name和arr都在closure中,而age被使用,所以age不存在于closure中。
隐式全局变量
全局变量通常不会被回收,所以要避免额外的全局变量,使用完毕之后重置为null即可。
例子如下:
function Person() {}
function fn() {
p1 = new Person();
this.p2 = new Person();
}
fn();
p1和p2均会被绑定在全局变量上,如果不手动将他们置为null,那么就会产生内存泄漏。
分离的DOM
当在界面中移除DOM节点时,还要移除相应的节点引用。
<body>
<div id="container">
<p id="title">title</p>
</div>
<script>
const container = document.getElementById('container');
const title = document.getElementById('title');
document.body.removeChild(container);
</script>
</body>
container节点已经在dom树上移除了,但还保持了引用,会造成内存泄漏,这种情况将container和title都置为null即可。
定时器
- setInterval
- setImmediate
- requestAnimationFrame
其实定时器算是因为闭包产生的,不过这里还是单独列出来了,因为定时器产生的内存泄漏是最为常见的。
例子如下:
function Person(name) { this.name = name };
function fn() {
const person = new Person('Rippi');
const timer = setInterval(() => {
person.age = 18;
}, 2000);
}
fn();
Map、Set对象
Map和Set存储对象时,如果不主动清除,也会造成内存泄漏的问题。
例子如下:
function Person() {};
let obj = new Person();
const set = new Set([obj]);
const map = new Map([[obj, 'Rippi']]);
obj = null;
如图,我们可以看到,尽管将obj置为了null,但由于new出来的person仍然被set和map引用着,如果不将set和map也置为null,这个new出来的Person是无法被回收的。
在一些场景下,我们可以使用WeakMap和WeakSet代替Map和Set,因为他们并不会增加引用计数,当所存的对象被置为空的时候,那么就会被回收掉。
我们看个例子:
function Person() {};
let obj = new Person();
const set = new WeakSet([obj]);
const map = new WeakMap([[obj, 'Rippi']]);
obj = null;
通过这两个小例子,可以看出很大的区别,WeakMap和WeakSet并没有产生内存泄漏。
以上就是我们常见的会产生内存泄漏的情况了。
排查内存泄漏
我们既然了解内存泄漏是什么,以及怎样会产生内存泄漏,那么我们还需要知道如何排查内存泄漏。
其实本文开头已经说了两种排查内存泄漏的方法了,一个是利用好devtool的内存模块,另外就是利用好调试模块。
这里再补充一个,那就是利用好performence模块。
我们通过一个简单的例子来看看都如何操作吧。
<body>
<div id="container">0</div>
<button id="click">click</button>
<script>
function Person() {};
const rows = [];
function getColumns() {
var col = new Array(100000).fill('0');
for (let i = 0; i < col.length; i++) {
col[i] = new Person();
}
return col;
}
click.addEventListener('click', () => {
rows.push(getColumns());
container.innerHTML = rows.length;
});
</script>
</body>
- 通过performence查看内存变化
js堆内存曲线一直向上走说明很可能产生了内存泄漏了,那么我们接下来就可以通过内存模块排查具体问题了。
- 通过内存快照
每一次快照内存都增加,而且就算手动GC(点击那个垃圾桶)也无法减少内存,说明肯定内存泄漏了。
- 对比快照找出问题
对比快照的功能在过滤器旁边的下拉框中,选择完成后,左侧可以选择对比的目标快照。
这里我们拿第四个快照和第三个快照进行对比。
这样就能定位到产生快照的大概位置了。
结尾
以上就是本文所有内容了,本文主要通过多个例子一一介绍了产生内存泄漏的场景,最后通过一个简单例子讲解了如何排查与定位内存泄漏。
最后,希望各位都能有所收获。
最后的最后,也希望看到此处的大家能给个小赞🌹