【JavaScript】垃圾回收与内存泄漏
? 专栏介绍
在现代Web开发中,JavaScript已经成为了不可或缺的一部分。它不仅可以为网页增加交互性和动态性,还可以在后端开发中使用Node.js构建高效的服务器端应用程序。作为一种灵活且易学的脚本语言,JavaScript具有广泛的应用场景,并且不断发展演进。在本专栏中,我们将深入学习JavaScript语言的基本语法、DOM操作、事件处理、异步编程以及常见算法和数据结构等内容。此外,我们还将介绍ES6及其后续版本中引入的新特性,如箭头函数、模块化、解构赋值等。通过学习这些内容,你将能够成为一名熟练的JavaScript开发者,并能够应用这些知识来构建出高质量和可维护的Web应用程序。让我们一起开始JavaScript之旅吧!
引言
JavaScript的垃圾回收机制是一种自动化的内存管理机制,用于检测和回收不再使用的内存资源,以便重新分配给其他需要的部分。JavaScript中的垃圾回收器负责跟踪和管理内存的分配和释放,使开发人员无需手动管理内存。
内存泄漏指的是程序中分配的内存空间无法被释放和回收,并且随着时间推移导致可用内存逐渐减少。
垃圾回收机制
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。
其原理是:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
不再使用的变量也就是生命周期结束的变量,当然只能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。
JavaScript中的垃圾回收机制主要基于以下两个原则:
1. 引用计数(Reference Counting)
这是一种简单的垃圾回收算法,它通过跟踪每个对象被引用的次数来确定是否是垃圾。当一个对象被引用时,引用计数加1;当一个对象不再被引用时,引用计数减1。当引用计数为0时,表示该对象不再被使用,可以被回收。 但是,引用计数算法无法解决循环引用问题。如果两个或多个对象相互引用,并且没有其他地方对它们进行引用,则它们的引用计数永远不会为0,导致内存泄漏。
2. 标记-清除(Mark and Sweep)
它通过标记活动对象并清除未标记对象来进行垃圾回收。
-
标记阶段
:从根对象(如全局变量、活动函数调用栈等)开始,垃圾回收器遍历对象图,并标记所有可达的对象。可达对象是指那些仍然被引用的对象。 -
清除阶段
:在标记阶段后,垃圾回收器清除未被标记的对象,即那些不再被引用的垃圾对象。这些未被标记的对象将被释放,并且内存空间可以重新分配给其他需要的部分。 -
压缩阶段(可选)
:在清除阶段后,可能会产生内存碎片。为了解决这个问题,垃圾回收器可
以进行内存压缩操作,将活动对象紧凑地放置在一起,以便更好地利用内存空间。
示例
标记清除
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其标记为“离开环境”。
function test(){
var a = 10 // 被标记 ,进入环境
var b = 20 // 被标记 ,进入环境
}
test() // 执行完毕 之后 a、b 又被标离开环境,被回收。
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
IE9+、Firefox、Opera、Chrome、Safari 的 JS 使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
引用计数
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
function test() {
var a = {}; // a 指向对象的引用次数为 1
var b = a; // a 指向对象的引用次数加 1,为 2
var c = a; // a 指向对象的引用次数再加 1,为 3
var b = {}; // a 指向对象的引用次数减 1,为 2
}
但是,如果循环引用就会造成内存泄漏的问题,例如:
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
以上代码 a 和 b 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。
内存泄漏
1. 未清理的定时器或事件监听器
function startProcess() {
setInterval(() => {
// 执行一些操作
}, 1000)
}
startProcess()
在上述代码中,我们创建了一个定时器,但没有清除它。每次定时器触发时,都会执行一些操作。如果我们没有在不再需要定时器时调用 clearInterval()
方法来清除它,定时器将持续运行并占用内存资源。
解决方法
function startProcess() {
const intervalId = setInterval(() => {
// 执行一些操作
}, 1000)
// 在不再需要定时器时清除它
setTimeout(() => {
clearInterval(intervalId)
}, 5000)
}
startProcess()
在上述代码中,我们使用 setInterval()
创建了一个定时器,并在5秒后使用 clearInterval()
清除它。这样可以确保在一段时间后停止定时器并释放相关资源。
2. 闭包中的循环引用
function createClosure() {
let data = "Sensitive Data"
return function() {
console.log(data)
}
}
let closure = createClosure()
在上述代码中,我们创建了一个闭包函数,并将其赋值给变量 closure
。闭包函数中引用了外部变量 data
。如果我们在使用完闭包函数后不解除对它的引用,则闭包函数和其引用的外部变量 data
将无法被垃圾回收。
解决方法
closure = null // 解除对闭包函数的引用
在上述代码中,我们将变量 closure
设置为 null
,解除了对闭包函数的引用。这样,在下一次垃圾回收周期中,闭包函数及其引用的外部变量将被标记为不再使用,并被释放。
3. 未释放的DOM元素事件监听器
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元素等,以避免内存泄漏问题。
总结
垃圾回收是一种自动化的内存管理机制,通过标记-清除和压缩等步骤来回收不再使用的内存资源。然而,如果代码中存在内存泄漏问题,可能导致垃圾回收器无法正确释放内存。为了避免内存泄漏,需要注意及时释放资源、避免循环引用,并确保显式地解除绑定和移除不再需要的对象。
😶 写在结尾
前端设计模式专栏
设计模式是软件开发中不可或缺的一部分,它们帮助我们解决了许多常见问题,并提供了一种优雅而可靠的方式来构建应用程序。在本专栏中,我们介绍了所有的前端设计模式,包括观察者模式、单例模式、策略模式等等。通过学习这些设计模式,并将其应用于实际项目中,我们可以提高代码的可维护性、可扩展性和可重用性。希望这个专栏能够帮助你在前端开发中更好地应用设计模式,写出高质量的代码。点击订阅前端设计模式专栏
Vue专栏
Vue.js是一款流行的JavaScript框架,用于构建用户界面。它采用了MVVM(Model-View-ViewModel)的架构模式,通过数据驱动和组件化的方式,使开发者能够更轻松地构建交互性强、可复用的Web应用程序。在这个专栏中,我们将深入探讨Vue.js的核心概念、组件开发、状态管理、路由和性能优化等方面的知识。我们将学习如何使用Vue.js构建响应式的用户界面,并探索其强大的生态系统,如Vue Router和Vuex、Pinia。通过学习这些内容,你将能够成为一名熟练的Vue.js开发者,并能够应用这些知识来构建复杂而高效的Web应用程序。点击订阅Vue专栏
JavaScript(ES6)专栏
JavaScript是一种广泛应用于网页开发和后端开发的脚本语言。它具有动态性、灵活性和易学性的特点,是构建现代Web应用程序的重要工具之一。在这个专栏中,我们将深入探讨JavaScript语言的基本语法、DOM操作、事件处理、异步编程以及常见算法和数据结构等内容。此外,我们还将介绍ES6(ECMAScript 2015)及其后续版本中引入的新特性,如箭头函数、模块化、解构赋值等。通过学习这些内容,你将能够成为一名熟练的JavaScript开发者,并能够应用这些知识来构建出高质量和可维护的Web应用程序。点击订阅JavaScript(ES6)专栏
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!