聊下内存泄露
前言
不知道各位前端大佬,平时有没有遇到过内存泄露,然后整个页面都卡死的情况呢?
我在最开始接手公司项目的时候就曾经遇到这种问题,页面切换多几遍就会很卡顿,后面排查才知道是因为定时器使用有问题,频繁创建最后导致内存泄露了。
这个也是面试的常见问题,所以我们这里来聊下内存泄露吧,什么情况下会导致内存泄露。
内存泄露是什么
内存泄露指在程序执行的过程中,系统分配的内存没有被正常释放,导致内存一直被占用浪费,轻则程序性能被影响,重则进程崩溃
需要注意内存泄露和内存溢出是不一样的,内存泄露是有部分内存已经无法控制了,而内存溢出指分配的内存不够
那为什么系统分配的内存会无法控制呢?
众所周知,我们在执行程序的时候会经历三个步骤:分配内存 > 使用内存 > 回收内存
。
我们平时看到突然内存激增,页面卡顿是否意味着内存泄露呢?不一定,有可能是代码写的不好,比如突然进行了大量占用内存的操作,但是如果后续内存可以正常回收,那就不算内存泄露。
对于js而言,它有一套自己的垃圾回收机制,内存泄露指即便程序不再需要这部分内存,但是垃圾回收机制依旧认为还在使用,所以无法回收这部分内存导致内存泄露
内存泄露本质上指:程序失去对申请的内存失去控制,无法释放。
常见的内存泄露场景
意外的全局变量
JavaScript常运行在浏览器环境中,this指向的是window,this上添加变量是属于全局对象window的,全局变量不会被回收,除非重新指向null。
因此如果我们意外定义了不需要的全局变量,就会导致出现部分无法回收的内存。 我们不妨验证下:
测试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="memory()">内存泄露</button>
<script type="text/javascript">
function memory() {
for (let index = 0; index < 10000; index++) {
this[index + 'key']=index
}
}
</script>
</body>
</html>
性能监控效果
可以清楚看到我们执行myFunction函数之后。进行了一万次往window上挂载变量的操作,当我们手动执行GC的时候会发现最后有一段内存下不来,意味着这部分内存已经泄露了。
console.log()
作为程序员我们看到console.log(),用来打印数据,定位错误啥的还是很方便的。
频繁使用console.log也会导致内存泄露,因为打印的对象需要被展示,因此log的时候会打印出去这个对象快照,当我们点开log内容时就会去内存读取实际的内容,因此一般情况下我们会清理生产环境的console.log
我们不妨验证下:
测试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="memory()">内存泄露</button>
<script type="text/javascript">
function memory() {
for (let index = 0; index < 10000; index++) {
console.log({a: 1});
}
}
</script>
</body>
</html>
性能监控效果
可以看到我们执行myFunction函数之后。进行了一万次log操作,当我们手动执行GC的时候会发现最后有一段内存下不来,意味着这部分内存已经泄露了。
闭包
闭包也是我们日常开发的常用技巧,举个例子函数A中定义函数B,函数B调用了在函数A内定义的变量,并且在函数A外部被调用,这就形成了闭包
。
为什么说闭包会导致内存泄露呢?
不妨写段代码演示下:
function test() {
let a = {}
for (let index = 0; index < 10000; index++) {
a[index + 'key']=index
}
let inner = function () {
return a
}
return inner
}
let obj
function Closure() {
obj = test()
}
上面的代码通过Closure函数触发开始。
我们通过js执行栈去解释这段代码,首先执行Closure函数与之相关的上下文入栈,随后执行test函数与之相关的上下文入栈,在test函数中返回了inner函数,由于inner函数中使用的变量a指向test函数上下文,所以test函数无法出栈,同样的因为Closure函数调用全局的obj变量,因此Closure函数也无法出栈,
只要obj函数不执行,test函数无法出栈,那么这部分内存就会一直无法释放,导致内存泄露。
测试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="Closure()">闭包</button>
<script type="text/javascript">
function test() {
let a = {}
for (let index = 0; index < 10000; index++) {
a[index + 'key']=index
}
let inner = function () {
return a
}
return inner
}
let obj
function Closure() {
obj = test()
}
</script>
</body>
</html>
性能监控效果
dom泄露
js和dom使用的引擎不同,因此我们需要明确js操作dom的时候是非常损耗性能的,这也是为什么很多前端框架使用虚拟节点。 日常开发过程中我们往往会通过js去缓存dom节点信息,比如:
var main = document.querySelector('.main')
当我们完成这一步之后其实这个节点信息就缓存到window下的main里面了。 所以即便我们进行了移除等操作,这个缓存的节点其实依然存在,也不会被回收,导致内存泄露。
测试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="main">
<button id="button" onclick="dom()">dom泄露</button>
</div>
<script type="text/javascript">
function dom() {
var test= document.querySelector('#button')
var main = document.querySelector('.main')
main.removeChild(test)
}
</script>
</body>
</html>
性能监控效果
转载自:https://juejin.cn/post/7232229178278412345