深入了解JS内存泄漏
一、前言
程序的运行需要内存,程序向操作系统申请内存空间必须提供。内存泄漏就是用不到的内存没有及时释放。前端对内存泄漏的关注并不多,但是了解内存泄漏,如何避免内存泄露是技能提升的必经之路。用户一般不会在同一个web页面停留过久,即使有一点内存泄漏,重新加载页面内存就会跟着释放。C、C++这类语言没有垃圾回收机制,开发人员直接控制内存的申请和释放。前端js引擎自带垃圾回收机制,但是如果对内存泄漏没有概念也会导致页面卡顿。
二、什么是内存
学习硬件的时候,老师提过计算机的内存是由大量的触发器组成,每个触发器都包含几个晶体管,能存储一个位。单个触发器可以通过唯一标识符寻址,因此我们可以读取或者覆盖内存。计算机的内存可以看成巨大的位数组,可以读/写。程序运行的时候,会向操作系统申请分配内存空间,程序运行完会视情况释放内存。
三、内存回收的必要性
字符串、数组、对象没有固定的大小,对它们进行动态存储分配。动态分配内存,动态释放内存,才能保证内存能够再次使用,否则,js解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
无用的内存还在占用,得不到释放和归还。严重的时候会导致无用内存持续递增,从而导致整个系统卡顿,造成系统崩溃。
四、内存的生命周期
内存也有生命周期,不管什么语言,包括C、C++等语言,内存按顺序可以分为三个周期:
内存分配—内存使用—内存释放
- 分配期
操作系统分配所需要的内存
- 使用期
使用分配到的内存,进行读操作或者写操作
- 释放期
释放不再需要的内存
五、什么是内存泄漏
内存泄漏简单来说就是不再使用的内存没有及时释放,得不到内存归还。
官方解释:内存泄漏是指由于疏忽或者错误造成程序未能释放已经不再使用的内存。内存泄漏并非指物理上的消失,而是应用程序分配内存后,由于计算错误或者设计错误等等,导致在释放该段内存之前失去了对该段内存的控制,从而造成内存浪费。
内存不再需要,没有经过生命周期的释放期,那么内存就存在内存泄漏
六、js的内存管理机制
C语言作为底层语言有底层管理内存的接口,malloc()申请内存,free()释放内存。
js在创建变量的时候就自动分配内存,并且会有垃圾回收机制在内存不用的时候释放它。js开发人员不需要手动申请和释放。
js的内存管理机制和内存的生命周期一一对应,也是三个时期:分配内存—使用内存—释放内存。
(1)分配内存
js定义变量的时候,就会自动分配内存。
//给number数值变量分配内存
let number = 12345;
//给string字符串分配内存
const string = 'gaby';
//给对象及其包含的值分配内存
const object = {
a: 1,
b: null
};
//给数组及其包含的值分配内存(类似对象一样)
const array = [1, null, 'gaby']
//给函数可调用的对象,分配内存
function fn(a) {
return a;
}
(2)使用内存
使用值的过程就是对分配内存进行读操作或者写操作。读取或者写入可以是一个变量也可以是一个对象的属性值,甚至是传递函数的参数。
//给number数值变量分配内存
let number = 12345;
//给string字符串分配内存
const string = 'gaby';
//给对象及其包含的值分配内存
const object = {
a: 1,
b: null
};
//给数组及其包含的值分配内存(类似对象一样)
const array = [1, null, 'gaby']
//给函数可调用的对象,分配内存
function fn(a) {
return a;
}
//写操作,写入内存
number = 23456;
//读操作,读取number和fn的内存,并且函数参数写入内存
fn(number);
(3)回收内存
垃圾回收机制GC回收内存。内存泄漏一般发生在这一步,缺少释放内存的生命周期。js垃圾回收机制可以回收大部分的内存,但是也存在一些疏忽回收不了的情况,这时候就需要手动清理内存。
上一篇笔记整理两个常用的垃圾回收机制算法:标记清除法、引用计数法
- 引用计数法
引用计数法对于对象循环引用无法回收,需要手动清除,设置null。
在上一篇笔记的基础上,增加一个知识点的补充,ES6的强引用和弱引用
强引用:强引用才有引用计数的叠加,只有计数0被垃圾回收,一般需要手动释放内存。
弱引用:弱引用并没有触发引用计数的叠加,只要计数为0,弱引用自动消失,无需手动回收内存。
- 标记清除法
变量进入环境标记“进入环境”,变量离开环境标记“离开环境”,变量进入环境表示正在被使用无法回收。只有标记“离开环境”的变量才可以被回收。这里的环境指的就是变量的作用域。对于全局变量,只有关闭页面才能被回收销毁。
七、js内存泄漏的场景详细总结
(1)意外的全局变量
解决办法,js文件头部加上’use strict’严格模式解析js,可以避免意外的全局变量。
//全局作用域定义,basicCount没被声明,变成全局变量,页面关闭之前不会被释放
function connt(number) {
basicCount = 2; //相当于window.basicCount = 2
return basicCount + number;
}
//this创建的意外全局变量
function fn() {
this.variable = 'accident'
}
//fn()调用自己,this指向全局对象
fn();
(2)被遗忘的计时器
解决方法:手动清除计时器
//id为Node的元素从DOM移除,计时器依然还在,回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放
var someResource = getData();
setInterval(function () {
var node = document.getElementById('Node')
if (node) {
//处理node和someResource
node.innerHTML = JSON.stringify(someResource)
}
}, 1000)
//vue组件,忘记清理计时器。销毁组件,setInterval还在运行,内存无法回收
export default {
methods: {
refresh() {
//获取数据
},
},
mounthed() {
this.refreshInterval = setInterval(function () {
//轮询获取数据
this.refresh()
}, 2000)
},
}
//手动清除计时器
export default {
methods: {
refresh() {
//获取数据
},
},
mounthed() {
this.refreshInterval = setInterval(function () {
//轮询获取数据
this.refresh()
}, 2000)
},
beforeDestroy() {//手动清除计数器
clearInterval(this.refreshInterval)
},
}
(3)闭包
解决方法:将事件处理定义在外部,解除闭包;定义事件处理函数的外部函数中,删除对DOM的引用
//闭包可以维持函数内部变量不被释放。函数内定义函数,内部函数事件回调引用外部函数,形成闭包
function bindEvent() {
var obj = document.createElement('xxx')
obj.click = function () {
//...
}
}
//将事件处理函数定义在外面
function bindEvent() {
var obj = document.createElement('xxx')
obj.click = onclickHandler
}
//在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
var obj = document.createElement('xxx')
obj.click = function () {
//...
}
obj = null
}
(4)被遗忘的事件监听器
解决方法:手动清除无用的事件监听器
//组件销毁,resize还在监听,内存无法回收
export default {
mounted() {
window.addEventListener('resize', () => {
//...
})
},
}
//销毁组件的时候移除相关事件
export default {
mounted() {
this.resizeEventCallback = () => {
//...
}
window.addEventListener('resize', this.resizeEventCallback)
},
beforeDestroy() {
window.removeEventListener('resize', this.removeEventCallback)
},
}
(5)被遗忘的ES6 Set成员
//内存泄漏,成员是引用类型,对象
let map = new Set()
let value = {
number: 123
};
map.add(value);
value = null
//修改
let map = new Set()
let value = {
number: 123
};
map.add(value);
map.delete(value);
value = null;
//或者用WeakSet弱引用,内存回收不考虑这个引用是否存在
let map = new WeakSet()
let value = {
number: 123
};
map.add(value);
value = null;
(6)被遗忘的ES6 Map键名
//内存泄漏
let map = new Map();
let key = new Array(1024 * 1024 * 1024);
map.set(key, 1);
key = null;
//修改
let map = new Map();
let key = new Array(1024 * 1024 * 1024);
map.set(key, 1);
map.delete(key);
key = null;
//弱引用
let map = new WeakMap();
let key = new Array(1024 * 1024 * 1024);
map.set(key, 1);
key = null;
(7)被遗忘的订阅发布事件监听器
订阅发布三个方法:emit、on、off
//组件销毁的时候,自定义test事件还在监听中,里面涉及到的内存无法回收
import customEvent from 'event'
export default {
methods: {
onclick() {
customEvent.emit('test', {
tyep: 'click'
})
},
},
mounted() {
customEvent.on('test', data => {
//逻辑处理
console.log(data)
})
},
}
//手动销毁移除相关事件
import customEvent from 'event'
export default {
methods: {
onclick() {
customEvent.emit('test', {
tyep: 'click'
})
},
},
mounted() {
customEvent.on('test', data => {
//逻辑处理
console.log(data)
})
},
beforeDestroy() {
customEvent.off('test')
},
}
(8)脱离DOM的引用
解决方法:手动清除引用
页面的DOM都占用内存。获取A页面,就能获取A页面的DOM对象。移除A页面的元素,这个元素因为一些原因无法被回收,就会造成内存泄漏。DOM节点保存数据结构,每一行DOM存成字典(JSON键值对)。
DOM元素有两个引用:①DOM数中 ②字典JSON键值对中
//buttom元素移除,内存占用仍然存在
class Test {
constructor() {
this.elements = {
button: document.querySelector('#button'),
div: document.querySelector('#div'),
span: document.querySelector('#span'),
}
}
removeButton() {
document.body.removeChild(this.elements.button)
}
}
const a = new Test()
a.removeButton()
//手动清除
class Test {
constructor() {
this.elements = {
button: document.querySelector('#button'),
div: document.querySelector('#div'),
span: document.querySelector('#span'),
}
}
removeButton() {
document.body.removeChild(this.elements.button)
this.elements.button = null//手动null
}
}
const a = new Test()
a.removeButton()
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function dostuff() {
button.click();
image.src = '1.jpg';
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
//还存在全局#button引用
//elements字典 button还在里面,不可回收
}
//手动null
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function dostuff() {
button.click();
image.src = '1.jpg';
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
document.getElementById('button') = null
//还存在全局#button引用
//elements字典 button还在里面,不可回收
}
八、如何发现内存泄漏
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="app">
<button id="run">运行</button>
<button id="stop">停止</button>
</div>
<script>
const arr = []
for (let i = 0; i < 20000; i++) {
arr.push(i)
}
let newArr = []
function run() {
newArr = newArr.concat(arr)
}
let clearRun
document.querySelector('#run').onclick = function () {
clearRun = setInterval(() => {
run()
}, 1000)
}
document.querySelector('#stop').onclick = function () {
clearInterval(clearRun)
}
</script>
</body>
</html>
①确定是否是内存泄露的问题
打开谷歌浏览器开发者工具,切换至 Performance 选项,勾选 Memory
选项 Screenshots
选项,点击运行
按钮
录制10秒后,停止,得到内存的走势
替换测试数组的数据,将20000换成20,重复操作,确实是内存泄漏
②查找内存泄漏的位置
重复录制,记录 JavaScript 堆内存,看看哪个堆占用的内存更高。
九、避免内存泄漏
①减少不必要的全局变量、生命周期较长的对象,及时进行垃圾回收处理
②写程序的时候避免“死循环”
③避免创建过多的对象