内存泄漏
什么是内存泄漏
内存泄漏是指无用的数据还占用内存,使得该内存无法释放和归还,内存泄漏情况严重时过多内存被占用会导致整个系统卡顿,甚至崩溃。
引擎中有垃圾回收机制,主要针对一些程序中不再使用的对象,对其清理回收并释放响应内存空间。
引擎针对垃圾回收做了各种优化,为了尽可能的确保垃圾能够被回收,但在开发过程中不当的操作会使的引擎并不能确认当前对象是否不会再被使用,无法进行回收,造成内存泄漏的情况。
相关概念
内存管理
像c语言这样的底层语言一般都有底层的内存管理接口,比如malloc()
和free()
。在javaScript
中在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时释放内存。释放的过程称为垃圾回收。内存泄漏就是指已经不再使用的变量,无法被垃圾回收。
内存生命周期
内存生命周期主要分为三个阶段:
- 分配我们所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放
所有语言的第二个阶段都是明确的,第一和第三部分在底层语言中是明确的,但在像JavaScript
这些高级语言中,大部分都是隐含的。
垃圾回收
大多数的内存管理问题都在这个阶段, 垃圾回收过程中最艰难的任务是找到哪些被分配的内存确实已经不再需要被了。垃圾回收器的主要工作就是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。但这个过程并不确认正确,因为要知道是否仍然需要某块内存是无法判断的。我们可以大致了解一下js中的垃圾回收算法。
引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限,叫做一个对象引用另一个对象。例如,一个JavaScript
对象具有对它的原型的引用和对它属性的引用。
引用计数垃圾回收机制
这是最初级的垃圾回收算法,此算法把对象是都不再需要简化定义为对象有没有被其他对象引用到。如果没有引用指向该对象(零引用),对象将被垃圾回收器回收。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
该垃圾回收算法也有相应的限制,当出现循环引用的情况,则无法进行正确的回收。IE6、7
是使用引用计数方式对DOM
对象进行垃圾回收,常常出现对象被循环引用引发的内存泄漏情况。
标记-清除算法
标记清除法,目前在JavaScript
引擎中,这种算法是最常见的,到目前为止的大多数浏览器的JavaScript
引擎都在采用标记清除算法,只是个大浏览器厂商还对此算法进行了优化加工。
此算法分为标记和清除两个阶段,标记阶段即为所有对象做上相应标记,然后根据标记对非活动对象进行销毁。
当代码执行在一个环境中时,每声明一个变量,就会对该变量做一个进入环境的标记,当代码执行进入另一个环境中时,也就是说要离开上一个环境,这时对于上一个环境中的变量会做一个离开执行的标记。当执行垃圾回收时,会根据标记来决定清除哪些变量。
常见的内存泄漏情况
不正当的闭包
这个不必多说,只要说到内存泄漏,就会联想到闭包。当内部函数访问外部函数的变量,且通过return
暴露出去的时候,当有外部变量引用到函数执行的结果时,该变量不会被回收。这并不属于内存泄漏,因为有时候确实有相应的需求。但是如果在使用后不将引用至空,且也不会再用到该未被回收的变量,就是造成了内存泄漏。所以在使用闭包的时候一定要注意对于被暴露出来的变量的回收。
function fn(){
let test = new Array(1000).fill('isboyjc')
return function(){
return test
}
}
let fnChild = f2()
fnChild()
fnChild = null
意外的全局变量
JavaScript
的垃圾回收是自动执行的,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。
但是对于全局变量,很难判断这些变量什么时候将不被需要,所以全局变量一般不会被回收,除非手动置空。所以一般要避免一些额外的全局变量的产生:
function fun(){
// 没有声明从而制造了隐式全局变量test1
test1 = new Array(1000).fill('array');
// 函数内部this指向window,制造了隐式全局变量test2
this.test2 = new Array(1000).fill('array');
}
fn()
没有清理的DOM引用
有时我们需要对DOM节点做缓存。在特定情况下对节点做修改和删除。当我们把DOM节点通过变量来缓存时,当前DOM节点有两个引用:一个在DOM树中,另一个是变量引用。如果我们需要对某个节点做删除操作,一定要把两个引用都清除。
<div id="root">
<ul id="ul">
<li></li>
<li></li>
<li id="li3"></li>
<li></li>
</ul>
</div>
<script>
let root = document.querySelector('#root')
let ul = document.querySelector('#ul')
let li3 = document.querySelector('#li3')
// 由于ul变量存在,整个ul及其子元素都不能GC
root.removeChild(ul)
// 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
ul = null
// 已无变量引用,此时可以GC
li3 = null
</script>
不谨慎的异步操作
这种情况也非常常见,当我们在组件中使用到setTimeout
和setInterval
时一定要注意。最容易出现的情况就是在一个已经卸载的组件的useEffect
中调用一个定时器,定时器中还触发了useState
更新状态,那么就会发生内存泄漏警告。
useEffect(() => {
let timer = setInterval(() => {
setTimingNum(timing+1)
}, 1000)
return () => {
clearInterval(timer);
};
}, [])
其实除了这种情况,我们也要避免在useEffect
中发起一些异步操作(例如发送请求等)后,在得到结果后进行setState
操作。因为如果在异步操作得到结果之前组件就被销毁了,再触发setState
也会造成内存泄漏。
未被清除的事件监听器
当事件监听器在组件内挂载事件处理函数,在组件销毁是需要主动将其清除,如果不做清除其中引用的变量或者函数都被认为是需要的而不会进行回收。
function App() {
const [count, setCount] = useState(0);
const onMouseDown = e => {
setCount(count + 1);
};
useEffect(() => {
document.addEventListener("mousedown", onMouseDown);
return () => {
document.removeEventListener("mousedown", onMouseDown);
};
}, []);
return (
<div className="App">
<h1>hello world</h1>
<h2>{count}</h2>
</div>
);
}
Map、Set对象
这个就涉及到强引用与弱引用了,相信在了解Map、Set
的时候也了解过WeakMap
、WeakSet
。WeakMap
只接受对象作为键名,WeakSet
只接受对象作为成员。map
的键值也可以为对象,set
的成员也是可以是对象。最重要的区别在于WeakMap、WeakSet
对于对象是弱引用,Map、Set
对于对象是强引用。
弱引用的情况下,垃圾回收机制是不会考虑当前引用的,也就是说如果其他对象都不再引用该对象,那么垃圾回收机制就会自动回收该对象所占的内存,不会考虑该对象还存在于WeakSet
中或者还在WeakMap
作为键值。
所以当我们在特定使用场景中,向往对象上添加数据,但是又不想影响垃圾回收机制,可以考虑使用WeakMap、WeakSet
。但是这个还是需要根据需求,具体分析要使用哪种结构,毕竟如果盲目使用WeakMap、WeakSet
导致数据丢失也是不合适的。
关于内存泄漏的跟踪方案目前还在摸索,由于真正的项目中函数系统对象各种对象非常多,并不容易定位追踪到具体的点。