引言
--
JavaScript的垃圾回收机制是一种自动化的内存管理机制,用于检测和回收不再使用的内存资源,以便重新分配给其他需要的部分。JavaScript中的垃圾回收器负责跟踪和管理内存的分配和释放,使开发人员无需手动管理内存。
内存泄漏指的是程序中分配的内存空间无法被释放和回收,并且随着时间推移导致可用内存逐渐减少。
垃圾回收机制
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。
其原理是:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
不再使用的变量也就是生命周期结束的变量,当然只能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。
JavaScript中的垃圾回收机制主要基于以下两个原则:
1. 引用计数(Reference Counting)
这是一种简单的垃圾回收算法,它通过跟踪每个对象被引用的次数来确定是否是垃圾。当一个对象被引用时,引用计数加1;当一个对象不再被引用时,引用计数减1。当引用计数为0时,表示该对象不再被使用,可以被回收。 但是,引用计数算法无法解决循环引用问题。如果两个或多个对象相互引用,并且没有其他地方对它们进行引用,则它们的引用计数永远不会为0,导致内存泄漏。
2. 标记-清除(Mark and Sweep)
它通过标记活动对象并清除未标记对象来进行垃圾回收。
标记阶段
:从根对象(如全局变量、活动函数调用栈等)开始,垃圾回收器遍历对象图,并标记所有可达的对象。可达对象是指那些仍然被引用的对象。清除阶段
:在标记阶段后,垃圾回收器清除未被标记的对象,即那些不再被引用的垃圾对象。这些未被标记的对象将被释放,并且内存空间可以重新分配给其他需要的部分。压缩阶段(可选)
:在清除阶段后,可能会产生内存碎片。为了解决这个问题,垃圾回收器可 以进行内存压缩操作,将活动对象紧凑地放置在一起,以便更好地利用内存空间。
示例
--
标记清除
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其标记为“离开环境”。
代码语言:javascript复制function test(){
var a = 10 // 被标记 ,进入环境
var b = 20 // 被标记 ,进入环境
}
test() // 执行完毕 之后 a、b 又被标离开环境,被回收。
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
IE9 、Firefox、Opera、Chrome、Safari 的 JS 使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
引用计数
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
代码语言:javascript复制function test() {
var a = {}; // a 指向对象的引用次数为 1
var b = a; // a 指向对象的引用次数加 1,为 2
var c = a; // a 指向对象的引用次数再加 1,为 3
var b = {}; // a 指向对象的引用次数减 1,为 2
}
但是,如果循环引用就会造成内存泄漏的问题,例如:
代码语言:javascript复制function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
以上代码 a 和 b 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。
内存泄漏
1. 未清理的定时器或事件监听器
代码语言:javascript复制function startProcess() {
setInterval(() => {
// 执行一些操作
}, 1000)
}
startProcess()
在上述代码中,我们创建了一个定时器,但没有清除它。每次定时器触发时,都会执行一些操作。如果我们没有在不再需要定时器时调用 clearInterval()
方法来清除它,定时器将持续运行并占用内存资源。
解决方法
function startProcess() {
const intervalId = setInterval(() => {
// 执行一些操作
}, 1000)
// 在不再需要定时器时清除它
setTimeout(() => {
clearInterval(intervalId)
}, 5000)
}
startProcess()
在上述代码中,我们使用 setInterval()
创建了一个定时器,并在5秒后使用 clearInterval()
清除它。这样可以确保在一段时间后停止定时器并释放相关资源。
2. 闭包中的循环引用
代码语言:javascript复制function createClosure() {
let data = "Sensitive Data"
return function() {
console.log(data)
}
}
let closure = createClosure()
在上述代码中,我们创建了一个闭包函数,并将其赋值给变量 closure
。闭包函数中引用了外部变量 data
。如果我们在使用完闭包函数后不解除对它的引用,则闭包函数和其引用的外部变量 data
将无法被垃圾回收。
解决方法
closure = null // 解除对闭包函数的引用
在上述代码中,我们将变量 closure
设置为 null
,解除了对闭包函数的引用。这样,在下一次垃圾回收周期中,闭包函数及其引用的外部变量将被标记为不再使用,并被释放。
3. 未释放的DOM元素事件监听器
代码语言:javascript复制const button = document.querySelector("#myButton")
button.addEventListener("click", () => {
// 执行一些操作
})
在上述代码中,我们给一个按钮元素添加了一个点击事件监听器。如果我们忘记在不再需要该按钮时移除事件监听器,该按钮元素将继续保持对事件监听器的引用,导致内存泄漏。
解决方法
const button = document.querySelector("#myButton")
function handleClick() {
// 执行一些操作
}
button.addEventListener("click", handleClick)
// 在不再需要按钮时移除事件监听器
button.removeEventListener("click", handleClick)
在上述代码中,我们使用 addEventListener()
添加了一个点击事件监听器,并在不再需要按钮时使用 removeEventListener()
移除它。这样可以确保在不再需要按钮时,相关的事件监听器被正确地移除,从而避免内存泄漏。
这些示例展示了一些常见的JavaScript内存泄漏场景。在实际开发中,我们应该注意及时清理不再使用的定时器、事件监听器、闭包和DOM元素等,以避免内存泄漏问题。
总结
--
垃圾回收是一种自动化的内存管理机制,通过标记-清除和压缩等步骤来回收不再使用的内存资源。然而,如果代码中存在内存泄漏问题,可能导致垃圾回收器无法正确释放内存。为了避免内存泄漏,需要注意及时释放资源、避免循环引用,并确保显式地解除绑定和移除不再需要的对象。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!