JavaScript的垃圾回收机制
简介
垃圾回收是一种自动的内存管理机制。当计算机中的动态内存不再需要时,就会予以释放。
因为计算机中的内存是有限的,如果变量、函数等动态内存只有产生而没有消亡的过程,那么内存被占满也只是时间问题罢了。
注:JavaScript
中的垃圾回收是自动进行的,但有些情况会导致一些没用的变量占据内存,无法自动被消除,后续会讲到。
方式一:引用计数
在内存管理环境中,对象A如果有访问对象B的权限,那么就叫做对象A引用对象B。
引用计数的策略是将 “对象是否不再需要” 简化成 “对象有没有其他对象引用到它”,如果没有其他对象引用这个对象,那么这个对象将会被回收。
例如:
let obj1 = { a: 1 }; // A对象({a:1})被创建,赋值给 obj1,A的引用个数为1
let obj2 = obj1; // A对象的引用个数变为2
obj1 = null; // A的引用个数变为1
obj2 = null; // A的引用个数变为0,此时A对象就可以被垃圾回收了
缺点:引用计数有个最大的问题:循环引用
function fn() {
let obj1 = {};
let obj2 = {}
obj1.a = obj2; // obj1 引用了 obj2
obj2.a = obj1; // obj2 引用了 obj1
}
当 fn
函数执行后,返回值是 undefined
,所以整个函数以及内部的变量都应该被回收,但根据引用计数的方式,obj1
和 obj2
的引用此时都不为 0,所以它们并不会被回收。
想要解决上述问题,需要在不使用它们的时候,手动将其设置为空:
function fn() {
let obj1 = {};
let obj2 = {}
obj1.a = obj2; // obj1 引用了 obj2
obj2.a = obj1; // obj2 引用了 obj1
obj1 = null;
obj2 = null;
}
方式二:标记-清除(可达性)
这是 JavaScript
中最常见的垃圾回收方式了。从 2012年起,所有现代浏览器都使用了 标记-清除的垃圾回收方式。
可达性就是指那些可访问或可用的值,它们被保证存储在内存中。
1、什么是标记-清除?
JavaScript
中有个全局对象,浏览器中是 window
。
在标记阶段,垃圾回收期将从这个全局对象开始,寻找所有从这个全局对象开始引用的其他对象,再寻找其他对象引用的对象...,对这些能从根找到的对象进行标记。
在清除阶段,就会清理那些没有被标记的对象。
比如上述的代码示例中,fn
函数的返回值是 undefined
的,如果外界没有参数对其进行接收的话,整个 fn
函数(以及内部的变量)都会被清理掉。
2、标记-清除的缺点
**(1)效率低:**在垃圾回收阶段需要去遍历检查每一个引用,如果代码中对象过多,会导致非常严重的效率问题
**(2)同步JS代码执行:**浏览器在标记的过程中 js
是不能执行的,垃圾回收会拖慢我们 js
的运行时间
3、优化点
**(1)分代垃圾回收:**可以将对象分为新生代和老生代,对于二者采用不同的回收策略。例如 window
对象(老生代),我们对于老生代的回收可以不用那么勤快;对于新生代,需要马上标记回收。
**(2)增量执行:**不需要一次性遍历完成全部的引用,每次只遍历一部分,提高性能。
**(3)空余时间执行:**可以在 js
执行完成之后再执行,这样就不会影响到 js
的执行效率了。
- 解决方案:标记-整理
标记-整理在标记阶段没有什么不同,但是在标记结束后,它会将活着的(未被清理的)对象往内存的一边移动,最后清理掉边界的内存。
不过这种方法因为多做了一步处理,效率肯定是没有标记-清除高的---计算机中很多做法都是相互妥协的结果。
内存泄露
程序的运行需要内存,当程序提出要求,操作系统就会供给内存。对于不再用到的内存,没有及时释放,就叫做内存泄露。
对于持续运行的应用程序,必须及时释放内存,否则随着内存占用越来越高,轻则影响系统性能,重则导致应用崩溃。
1、循环引用
一旦数据不再使用,最好手动将其的值设置为 null
那么标记-清除法是如何解决循环引用问题的呢?
假如对象 A
和 对象 B
是相互引用的,但是它们俩和其他对象没有任何引用关系了,那么在 标记-清除 过程中(从 root
开始寻找)是无法到达 A、B
两个对象的,所以这俩就变成了 “可回收对象”
2、全局变量
- 正常的函数如下:
function fn() {
const name = "hayes";
}
fn()
当 fn
函数执行后,变量 name
就会被标记为可回收。
因为当函数执行时,内部创造了一个作用域来让函数内部的变量在其中声明。进入这个作用域后,浏览器就会为变量 name
创建一个内存空间,当 fn
执行完成后,其创建的作用域中的变量也都会被标记为可回收(垃圾),在下一个垃圾回收周期时就会被清理掉了。
- 不正常的函数:
function fn() {
name = "hayes"
}
fn()
上述代码可能是无意中声明了一个全局变量,导致 name
的作用域变成了 window
,所以 fn
函数执行完成后,name
也不会被垃圾回收机制清理掉。
- 另外一种不正常的函数:
function fn(name) {
this.name = name
}
fn("hayes")
在 fn
执行期间,内部的 this
实际上是 window
(因为fn
的调用者是 window
),这里犯的错误和上面一致(无意中声明了全局变量)
3、定时器
let name = "hayes"
setInterval(() => {
const node = document.querySelector("#name");
if(node) {
node.innerText = name
}
}, 1000)
上述代码中,每隔一秒就将 name
更新到 DOM
中。在 setInterval
没有结束前,回调函数中的变量已经回调函数本身都无法被回收。
只有调用了 clearInterval
,这个定时器才是结束了。
如果回调函数内没做什么事情,并且整个程序也没有调用 clearInterval
的话,就会造成内存泄露(这个定时器是无法被清理的)。
4、DOM引用
- 示例1:
当我们需要多次访问同一个 DOM
元素时,一般会将其存储在一个变量中,因为访问 DOM
的效率比较低,应该避免频繁地访问 DOM
元素
const button = document.querySelector("#button")
当我们不需要这个按钮时(需要删除按钮)
document.body.removeChild(document.querySelector("#button"))
看上去是删除了这个 DOM
元素,但这个 DOM
元素仍然被 button
变量引用,所以在内存层面,这个 DOM
元素是无法被回收的。
正确的做法是在使用结束后,将 button
设置为 null
- 示例2:
const li = document.querySelector(".li-active")
// 操作..
document.body.removeChild(document.querySelector("ul"))
我们在代码中保存了 ul
列表中的某一项 li
的引用。在使用完成后,将整个 ul
列表移出 DOM
,我们会认为内存仅仅会保存特点的这个 li
,而将其他列表项都清理掉。但事实并非如此,因为这个特点的 li
是 ul
的子元素,子元素与父元素之间是引用关系,所以如果内存中保留有 li
的引用,那么整个 ul
也将会继续呆在内存中。
总结
- 垃圾回收的方式
- 引用计数
- 标记-清除(多数浏览器的实现)
JavaScript
中可能存在的内存泄露- 全局变量
- 定时器
- DOM引用
转载自:https://juejin.cn/post/7126126410606837773