潜在的内存泄露
本文正在参与 “性能优化实战记录”话题征文活动
引言
什么是内存泄露?
系统进程不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak). 当内存占用越来越高,轻则影响系统性能,重则导致进程崩溃. 站在前端的视角.写的不好的 JavaScript 可能出现难以察觉且有害的内存泄露问题:
- 白屏 / 黑屏
- 卡顿 / 页面加载迟缓
- 闪退 / 发热
- 💥
哪些操作会造成内存泄露
一、意外声明全局变量
意外声明全局变量是最常见但也最容易解决的内存泄露问题. 在 window上创建的属性,只要 window 本身不被清除就不会消失. 无法做到有效地释放清除
- 代码编写
function t(){
name = 'renlingxin'
}
//name 变量会挂在到window上 而window上的变量在窗口关闭或重新刷新页面之前并不会销毁
// 用let const var 代替
- 其他挂载
插件 / 业务场景中有时会通过挂载到 window 的属性 实现运行环境下 不同工程之间地相互通信 例如
- WebView / JavaScript
- Cocos / JavaScript
- SDK - A / SDK - B
- ...
我们需要合理正确的挂载. 避免挂载大体积的数据.
二、闭包
function d(){
let name = 'renlingxin'
return function r(){
console.log(name)
}
}
//这会导致分配给name的内存被泄露.如果name很大.就会是一个大问题.
三、定时器(做到及时的clear)
保持良好的代码习惯.
<template>
<div>{{ timeDom }}</div>
</template>
<script>
mounted() {
this._time = setInterval(() => {
this.timeDom = new Date()
}, 1000);
},
// beforeDestroy 销毁之前的定时器
beforeDestroy() {
clearInterval(this._time)
}
</script>
四、事件监听器(例如vue的eventBus)
import eventBus from './eventBus'
export default {
mounted() {
// 添加 eventBus 监听器
eventBus.$on('page-change', () => {
console.log('page-change triggered!')
})
},
beforeDestroy() {
// 移除 eventBus 监听器
eventBus.$off('page-change')
}
}
五、变量和变量 / 变量和Dom 相互引用
我们来看一个例子:
- 在控制台执行 node --expose-gc
> process.memoryUsage()
{
rss: 143835136,
heapTotal: 5054464,
heapUsed: 3446128,
external: 1668772,
arrayBuffers: 9917
}
> let y = new Array(5*1024*1024)//1. 创建一个5*1024*1024大小的数组
undefined
> let g = {d:y,g:'renlingxin'} //2. 设置g中的key - d 值为y 建立强引用关系
undefined
> process.memoryUsage() //3. 查看当前内存为 43.48 M 左右
{
rss: 325320704,
heapTotal: 47263744,
heapUsed: 45596216,
external: 1668828,
arrayBuffers: 9933
}
> y = g.d = null // 4. 将 y 和 g.d 设置null 解除引用关系
null
> global.gc() // 5. 手动触发一次 gc 回收
undefined
> process.memoryUsage() // 6. 内存缩小至 3.02 M 左右
{
rss: 140541952,
heapTotal: 5316608,
heapUsed: 3171248,
external: 1668812,
arrayBuffers: 9917
}
以上我们可以得出结论 变量之间的相互引用 以及 主动释放引用关系从而触发GC回收的重要性
- 可以使用 weakMap / weakSet 建立弱引用关系
> process.memoryUsage() //1. 操作之前查看初始内存大小约为 3.28M
{
rss: 144654336,
heapTotal: 5054464,
heapUsed: 3447472,
external: 1668772,
arrayBuffers: 9917
}
> let t = new WeakMap() //2. 新建 WeakMap
undefined
> let y = new Array(5*1024*1024) //3. 新建 5*1024*1024 的数组
undefined
> t.set(y,'333') // 4. 设置 y 至 WeakMap
//WeakMap { <items unknown> }
> process.memoryUsage() // 5. 当前内存大小约为 43.22M
{
rss: 329957376,
heapTotal: 47001600,
heapUsed: 45320184,
external: 1668824,
arrayBuffers: 9929
}
> global.gc() // 6. 主动触发gc回收
undefined
> process.memoryUsage() //7. 内存无变化
{
rss: 332529664,
heapTotal: 47263744,
heapUsed: 45110040,
external: 1668812,
arrayBuffers: 9917
}
> y = null // 8. 将 y 原始值 清空为null.但是这里我们并没有去清空 t 中的 y
null
> global.gc() // 9. 释放内存
undefined
> process.memoryUsage() //10. 查看内存大小约为 3.07M
{
rss: 161005568,
heapTotal: 5316608,
heapUsed: 3227656,
external: 1668815,
arrayBuffers: 9920
}
六、console(浏览器会记忆输出的变量。避免在线上环境打印)
uglifyjs-webpack-plugin / transform-remove-console ... 等方法都可以实现移除 log
调试工具
一、Performance
全面的性能监控工具
名词解释
- Summary:各指标时间占用统计报表
- Bottom-Up:事件时长排序列表
- Call tree:事件调用顺序列表
- Event Log:事件发生的顺序列表
Ps:当你的Js Heap 有了这样的走势.那么就会有内存泄露的风险
二、 Memory
快照功能
-
开始拍照
-
全部删除
-
功能:
- Summary(概要) - 通过构造函数名分类显示对象;
- Comparison(对照) - 显示两个快照间对象的差异;
- Containment(控制) - 可用来探测堆内容;
-
Constructor(构造函数)表示所有通过该构造函数生成的对象
-
Distance 到 GC roots (GC 根对象)的距离。GC 根对象在浏览器中一般是 window 对象,在 Node.js 中是 global 对象
-
Shallow size 列显示了由对应构造函数生成的对象的shallow sizes(直接占用内存)总数 --- 对象自身的大小,不包括它引用的对象单位是字节
-
Retained size列展示了对应对象所占用的最大内存 ---- 对象自身的大小和它引用的对象的大小,即该对象被 GC 之后所能回收的内存大小。单位是字节
-
内存类型
- Compiled code 编译代码
- closure 闭包
- HTMLDivElement、HTMLAnchorElement、DocumentFragment等等这些其实就是你的代码中对元素的引用或者指定的 DOM 对象引用 ...
三、Performance monitor
实时监控工具.参考指标有 JavaScript 内存 / CPU 占用率 / DOM 节点 / 事件监听 ...
结论
内存泄露这个话题在前端提及的频率不是很多.大多数的业务场景是简单的页面h5的开发.但对于逻辑复杂 / 沉淀多 / 动画丰富等场景内存管理便尤其重要. 并且造成内存泄露的原因很多是积少成多的问题叠加.造成积重难返的境遇.因此需要我们从日常写码中重视内存管理.