JavaScript基础回顾查漏补缺——第二期
JavaScript的垃圾回收机制与内存泄漏浅析
1. 垃圾回收机制
垃圾回收是什么
- 垃圾回收(GC:Garbage Collection)指的是找到不再使用的变量,并释放掉它们所指向的内存。
- JavaScript中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
- 如果存在不再用到的内存,没有被释放,就叫做内存泄漏
垃圾回收的方式
标记清除(mark and sweep)
最常用的算法,大多数浏览器的JavaScript引擎都在采用标记清除算法。当触发垃圾回收的时候,会对可访问到的对象都会标记,然后清除所有没有标记的对象。
- 标记:垃圾回收器会从根对象开始遍历(通常以全局对象window做为根)。每一个可以访问到的对象都会被添加一个标识。
- 清除:垃圾回收器会对堆内存从头到尾进行线性遍历,有标识的对象清除标识,没标识的对象回收内存。
引用计数(reference counting)
跟踪记录每个变量值被使用的次数,跟随这个变量的使用情况,它的引用次数会产生改变,或者回收。
引用次数记录规则:
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就
为 1。 - 如果同一个值又被赋给另一个变量,那么引用数
加 1。 - 如果该变量的值被其他的值覆盖了,则引用次数
减 1。 - 当这个值的引用次数变
为 0的时候,就会被垃圾回收器回收。
分代式垃圾回收
Chrome 浏览器所使用的 V8 引擎(V8引擎是Google开源的JavaScript和WebAssembly引擎)采用的就是分代回收策略。该策略通过区分新生代(young generation)与老生代(tenured generation)将堆内存分为两个区域,采用不同的垃圾回收器即不同的策略管理垃圾回收。
新生代策略
新生代使用的是scavenge算法,具体实现是cheney算法。它把内存空间一分为二,使用区(From)和空闲区(To),新加入对象会分配到使用区,当使用区空间满的时候进行垃圾回收。
- 对使用区对象使用标记清除策略,标记了仍在使用对象复制到空闲区,然后直接释放掉使用区,然后空闲区和使用区翻转。
- 在下一次垃圾回收机制时,还被标记为使用对象,或者空闲区占用空间大于25%(因为空闲区会翻转为使用区,需要保留一定的空间),也会晋升为老生代。
老生代策略
老生代中使用的是标记清除算法,但是有对应的优化,综合使用了增量标记、懒性清理、并发回收三种方式
-
增量标记
增量标记就是将一次GC标记的过程,分成了很多小步,与应用逻辑交替执行,多次后完成一轮标记利用三色标记法(暂停与恢复)和写屏障(增量中修改引用)解决交替执行的问题。 -
懒性清理
增量标记完成后,就开始惰性清理。若当前的可用内存足够快速的执行代码,则先执行JavaScript脚本代码,延迟清理过程,另外也可以按需逐一清理标记的垃圾对象的内存,都清理完毕后面再接着执行新一轮增量标记 -
并发回收
并发回收指的是主线程在执行JavaScript的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起。
2. 内存泄漏相关问题
内存泄漏定义
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏的原因和解决
(1) 意外的全局变量
- 原因:JavaScript处理未定义变量会在全局对象创建一个新变量,在浏览器中,则在全局对象window中创建。
- 解决:开启严格模式'use strict',可以有效地避免上述问题。
- 注意:全局变量很难判断什么时候不再使用,无法正常回收,所以用来临时存储大量数据的全局变量,在确保处理完这些数据后要将其设置为null或者重新赋值。
(2) 没有清理DOM元素引用
- 原因:进行 DOM 操作的时候使用变量缓存 DOM 节点的引用,移除节点的时候没有释放对应的缓存引用,只释放了父节点的缓存则游离子树仍被引用无法释放。
- 解决:移除节点后,同时置空对应的缓存引用与引用子节点的变量
(3) 被遗忘的计时器和回调函数
- 原因:定时器
setInterval或setTimeout在不需要使用的时候,没有被清除,导致定时器的回调函数及其内部依赖的变量都不能被回收。 - 解决:当不需要定时器的时候,调用
clearInterval或clearTimeout移除。 - 注意:
requestAnimationFrame同样存在这种情况,结束使用后也需要调用cancelAnimationFrame。
(4) 被遗忘的事件监听器
- 原因:在组件内挂载的事件监听器(如dom.addEventListener、eventBus.on),在组件销毁时不主动清除,引用的变量或函数都不会进行回收。
- 解决:在组件销毁前使用生命周期钩子函数清除对应的监听器(如dom.removeEventListener、eventBus.off)。
(5) 不正确的闭包使用方式
- 原因:在闭包中引用了父函数中的变量。
- 解决:在闭包使用完之后将其赋空。
- 示例:
//正常的闭包使用方式
function fn1(){
let test = 123
//闭包内无引用父函数内变量
return function(){
console.log('123')
}
}
let fn1Child = fn1()
fn1Child()
//错误的闭包使用方式
function fn2(){
let test = 123
//闭包内引用了父函数内变量
return function(){
console.log('123')
//此处引用了变量
return test++
}
}
let fn2Child = fn1()
fn2Child()
//解决方式:使用完后补上赋空
fn2Child = null
参考文章: