1.内存泄漏
1-1.什么是内存泄漏:
不再用到的内存,没有被及时释放,就叫做内存泄漏
1-2.造成内存泄漏的原因:
1-2-1. 闭包使用不合理
function fn2(){
let colors = ['res', 'yellow', 'blue']
return function(){
console.log(colors)
return colors
}
}
let fn2Child = fn2()
fn2Child()
当函数fn2执行完成时,变量colors没有被回收,造成了内存泄漏。修改方法:
function fn2(){
let colors = ['res', 'yellow', 'blue']
return function(){
console.log(colors)
return colors
}
}
let fn2Child = fn2()
fn2Child()
// 函数fn2Child执行完成后,将其置为null,此时colors变量就可以被垃圾回收机制回收回收
fn2Child = null
1-2-2.隐式创建的全局变量
function fn2(){
// colors没有使用var/let/const声明,会将colors创建为全局变量(window的属性),即使函数fn2执行完成后,变量依然不会被垃圾回收机制回收。
colors = ['res', 'yellow', 'blue']
// 此处this指向全局对象(window),与colors同理不会被垃圾回收机制回收。
this.names = ['zhangsan', 'lisi', 'wangwu']
}
fn2()
在使用全局变量后,要注意将其置为null,否则其占用的内存空间无法被释放,造成内存泄漏。
1-2-3. 未清理的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>
1-2-4.遗忘的定时器
// 获取数据
let names = ['zhangsan', 'lisi','wangwu']
let count = 0
setInterval(() => {
if(count < 3) {
console.log(names[count])
count ++
} else {
count = 0
}
}, 1000)
在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。如果没有被 clear 掉的话,就会造成内存泄漏(回调函数及其依赖的变量都无法被回收)。
2.垃圾回收机制
2-1.不同语言的垃圾回收策略
垃圾数据回收分为手动回收和自动回收两种策略。
手动回收:如 C/C++ 就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的。
自动回收:如 JavaScript、Java、Python 等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。
2-2.调用栈中的数据是如何回收的
ESP:记录当前执行状态的指针,指向调用栈中函数的执行上下文,表示当前正在执行的函数。
调用栈用于存储函数执行上下文,当一个函数执行完成时,ESP会向下移动,指向下一层的执行上下文。这个下移操作就是销毁已经执行完成的函数的执行上下文的过程。
2-3.堆中的数据是如何回收的
2-3-1.代际假说
- 大部分对象在内存中存在的时间都很短。
- 不死的对象,会活得更久。
大多数的动态语言都符合上述规律。
2-3-2.分代收集
JavaScript将对内存分为两块区域:新生代区域、老生代区域。
新生代存放生存时间短的数据;老生代存放生存时间长的数据。
副垃圾回收器:负责新生代中的垃圾回收。
主垃圾回收器:负责老生代中的垃圾回收。
2-3-3.垃圾回收的工作流程
无论是副垃圾回收器还是主垃圾回收器,进行垃圾回收过程主要有3个步骤:标记-清除-整理。
标记:将堆内存中的数据标记为活动对象和非活动对象。
清除:标记完成后一次性回收内存中被标记为可回收的对象。
整理:可回收对象被清除后,会产生一些内存碎片,当内存碎片过多且需要分配一大块内存出来时,会出现内存不足的现象。所以需要进行内存整理。(注意:有的垃圾回收过程中不会产生内存碎片,也就不需要整理。副垃圾回收器就不需要。)
2-3-4.副垃圾回收器
副垃圾回收器负责新生区的垃圾回收。
Scavenge算法:将新生区分为对象区和空闲区,当新的数据需要内存时,会在对象区内分配内存,当对象区内存快满时,对对象区进行垃圾回收处理,然后将对象区内仍然存在的数据复制到空闲区(按顺序排好,所以复制过程就消除了内存碎片),最后将对象区和空闲区角色互换。
新生区内存较小:运行Scavenge算法需要进行数据的复制,内存太大会导致复制的数据量太大导致执行效率变低。
对象晋升策略:当经过两次垃圾回收操作后让然存在的数据,这些数据会被存放到老生区中。
2-3-5.主垃圾回收器
老生区中对象特点:数据较大、存活时间长。
标记-清除算法:从一组跟元素开始,递归遍历这组跟元素,遍历到的数据标记为活动对象,没有遍历到的数据标记为垃圾数据。标记完成后进行垃圾清除操作。最后再进行整理操作消除内存碎片。
2-4.全停顿
全停顿:主线程在运行JavaScript代码过程中,当要进行垃圾回收操作时,会暂停JavaScript代码的运行,等垃圾回收操作完成后再运行JavaScript代码。
全停顿影响:当垃圾回收操作时间较长时,会使页面出现明显卡顿。
增量标记算法:v8将垃圾回收标记过程拆分成一个个子标记过程,让子标记过程和JavaScript代码的运行交替进行,直到标记完成。
增量标记算法优点:降低垃圾回收过程中页面产生的卡顿感。