什么是内存泄露
-
代码的运行离不开内存,JavaScript程序每次创建字符串、数组或对象时都会分配内存来存储实体。
-
但是我们的内存是有额度的,不能无限使用,如果一个对象不被使用时,内存就会被释放掉。
-
JavaScript中的内存管理是自动执行的,当遇到这种情况时,js引擎将会帮我们把这部分垃圾清除掉,这也就是我们常说的垃圾回收。
-
那什么会被认为是垃圾呢?一般来讲当一个对象不在被GC根引用时就会被清除。GC根可以是浏览器的window,也可以是nodejs中的global。
-
但是垃圾回收不是万能的,如果我们写的逻辑不合理,导致一些并不会被使用的对象,一直被根对象引用,这是啥就会引起内存泄露。
-
一旦超过浏览器的承受范围,浏览器就会崩溃白屏。
我们敌人是 内存泄露
最终的大boss是 一直拿着垃圾不让它被回收的坏人!
Memory面板初探
- 一般我们能很容易发现严重的内存泄露问题 毕竟都白屏了,但是很难定位到是哪里导致的内存泄露,往往一个白屏问题 会牵扯到多处内存泄露。
- 我们要如何发现呢?我们的武器是?
- Memory面板
- Memory面板中毕竟常用的两个功能是Heap snapshot和Allocation instrumentation on timeline
- Memory面板
我们先从Memory面板的Heap snapshot讲起
-
我们先创建一个html,里面仅有一段js
<!DOCTYPE /> <html> <body> <script> function Fruit(name) { this.name = name; } var apple = new Fruit('apple'); </script> </body> </html>
-
我们打开这个页面 进入memory面板
如何记录一次Heap snapshot ?
1. 打开控制台的Memory面板 选择Heap snapshot
- 点击开始按钮 开始记录
上面的圆形按钮和下面的“Take snapshot”按钮都可以点击
3. 查看记录结果
点击后 就可以看到我们当前的内容情况了
如何看数据?
但是问题来了这个要怎么看呢,我们来一点点说明
- 首选我们要知道的是这个记录是当前所有的内存情况,并根据constructor分组,有一些是我们无法直接调用的比如“(system)”或者“(compiled code)”,当然也有一些可以直接访问,我们比较熟悉的如“Math” “String”等。
- 我们可以通过上方的搜索按钮,找到我们的类名Fruit,或者更直接下拉查看。
- 我们先看上半部分
- 第一列Constructor:
- 就像我之前所说的那样,对象会根据constructor分组
- 点开分组后能看到当前类下的所有对象
- 第二列 Distance
- 与GC根的距离
- 第三列 Shallow size
- 对象本身占用内存的大小,不包含其引用的对象
- 由其成员变量的数量和类型决定
- 第四列 Retained size
- 被GC回收的大小
- 对象及其依赖对象的内存大小
- 第一列Constructor:
- 再来看下半部分
- 下半部分展示了该对象都被哪些对象引用
- 比如我们可以看到Fruit(apple)被window上的apple所引用
- 比如我们可以看到Fruit(apple)被window上的apple所引用
- 下半部分展示了该对象都被哪些对象引用
如何对比?
当前,只有一次记录是无法很好的帮助我们定位内存泄露问题的,Memoery面板提供了对比功能,这样我们可以很方便的对比两次内存的记录
- 比如我们把之前的代码稍微修改一下,增加个按钮
<!DOCTYPE />
<html>
<body>
<button id="btn">add fruit</button>
<script>
const fruits = [];
const FRUIT_NAME = ['apple', 'orange', 'banana', 'peach', 'grape'];
const addBtn = document.getElementById('btn');
function Fruit(name) {
this.name = name;
}
function getRandomFruit() {
const random = Math.floor(Math.random() * FRUIT_NAME.length);
const fruitName = FRUIT_NAME[random];
return new Fruit(fruitName);
}
addBtn.addEventListener('click', () => {
const fruit = getRandomFruit();
fruits.push(fruit)
});
</script>
</body>
</html>
- 打开页面后,我们记录一次,点击几次按钮后再记录一次,
- 这样我们在右侧或得了两条记录, 当我们想要查看者两条记录的差异时可以
- 右侧点击第二次记录
- 上方左侧选择对比Comparison
- 上方右侧选择第一次记录
- 这样我们可以清晰的在面板中,Delta一列中看到Fruit增加的个数
Memory面板的Allocation instrumentation on timeline
-
Memory面板除了可以记录当前的一个内存情况,也可以动态记录内存
-
这样点击右侧的Profiles中,回到选择记录方式的页面
-
和上面一样 点击右侧圆形按钮或者点击下方的“Start”按钮都可以开始记录,
-
点击按钮开始记录后,我们再点几次按钮,可以看到上面有很多柱形,随着按钮的点击而变化
-
按右侧红色圆形按钮,结束记录
我们来解释下这个小条的含义,
柱形的总高度代表当时代码执行时所分配的所有内存
柱形灰色部分,代表已经被回收释放的内存
柱形蓝色部分,代表还依然被占用的内容 -
鼠标点击对应柱形,可以在下方看到和Take Snapshot中一样的内存分配情况
- 我们可以看到,此时Fruit被fruits数组所引用
代码中常见的内存泄漏(vue为例)
1. vue中beforeDestroy 生命周期单词写错
- beforeDestroy 和 destroy 单词写错
```
beforeDestory 错
beforeDestroy 对
```
2. EventListener 事件
- 忘写removeEventListener 事件
- addEventListener事件参数不匹配
- 参数不要写箭头函数
```
window.addEventListener('resize', this.resetHeight, true);
window.removeEventListener('resize', this.resetHeight); //错
window.removeEventListener('resize', this.resetHeight, true); //对
document.addEventListener('mousedown', e => { //错
const clientY = e.clientY;
if (clientY > 0 && clientY < 70) {
dragWindowHandle(true);
}
});
```
3. 节流防抖
- 使用节流防抖时,注意添加和移除事件的参数,引用保持一致
```
window.addEventListener('resize', Utils.debounce(this.screenResizeHandle, 200), true); //错
this.screenResizeHandle = Utils.debounce(this.screenResizeHandle, 200);
window.addEventListener('resize', this.screenResizeHandle, true); //对
window.removeEventListener('resize', this.screenResizeHandle, true);
```
4. 记得清除定时等引用变量 有可能造成闭包
```
timer = setTimeout(() => {
this.flag = true;
}, 1000);
beforeDestroy() {
clearTimeout(timer);
}
```
5.发布订阅 $on 以后记得 $off
```
created() {
EventBus.$on(EventTypes.SET_DOWNLOAD_FILES, this.getFileList);
},
beforeDestroy() {
EventBus.$off(EventTypes.SET_DOWNLOAD_FILES, this.getFileList);
}
```
6.记得卸载第三方引用
created() {
window.Offline.on('down', this.offlineUpload);
window.Offline.on('up', this.onlineUpload);
},
beforeDestroy() {
window.Offline.off('down', this.offlineUpload);
window.Offline.off('up', this.onlineUpload);
}
};
后语
好文章要分享给大家,转载 / 作者: 狐狸鹿妹子 百度