内存管理是指在声明一个变量时,我们为它分配一块内存空间,当不在需要时,将这块内存释放。在JS中开发者不需要手动进行内存管理,JS引擎会自动进行内存管理。
一、内存分配
当我声明变量时,我们要根据变量的数据类型进行内存分配。数据类型分为基本数据类型和引用数据类型。👇下面是一个简单的区别图解
基本类型:系统会为分配一块内存(栈),这块内存中保存的就是变量的内容。【String,Number,Boolean,Null,Undefined,Symbol】
引用类型:其存储的只是一个地址而已(栈),这个地址指向的内存块(堆)才是是变量的真正内容。【Object,Array,Function】
🪐:这里只简述一下数据类型在内存中的保存方式,其他关于数据类型的相关内容就不细说了。
二、内存释放(垃圾回收机制)
内存释放,即垃圾回收机制。当变量不再需要时,JS引擎把变量占用的内存回收。我们就需要知道如何判断变量不再需要。JavaScript 中有全局对象,浏览器中的全局对象是window
,垃圾回收机制时从全局对象开始。
有一个官方解释叫可达性,我可太讨厌用概念来解释概念了。直接上图👇根据内存图,我们可以把可达性理解成“无法到达的岛屿🏝”
内存状态图解
情况一:
let user = {name: 'John'}; // 一个对象 {name:”John”}(称之为对象A)被创建,全局变量 “user” 引用了对象A
user = null; // 如果user被覆盖,这个引用就没了
// 现在 对象A 变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据,然后释放内存。
情况二:
let user = {name: 'John'}; // 一个对象 {name:”John”}(称之为对象A)被创建,全局变量user引用了对象A
let admin = user; // 全局变量admin引用了对象A
user = null; // user被覆盖引用没了,但是对象A仍然可以通过 admin 这个全局变量访问到,所以对象还在内存中。
admin = null; // 如果继续覆盖了 admin,对象就会被删除。
情况三:
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry( {name: "John"}, {name:"Ann"} );
// 现在移除两个引用
delete family.father;
delete family.mother.husband;
family = null;
两种方式
1.引用计数法:如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
let obj = { name: 'John' }; // 一个对象(称之为 A)被创建,赋值给 obj,A 的引用个数为 1
let user = obj; // A 的引用个数变为 2
obj = null; // A 的引用个数变为 1
console.log(user) // 此时输出结果为{ a: 1 }
user = null; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了
局限性⚠:无法处理循环引用
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
2.标记清除法
垃圾回收机制时从全局对象开始,【开始念经】找所有从这个全局对象开始引用的对象,再找这些对象引用的对象......对这些活着的对象进行标记,这是标记阶段。【有点绕】清除阶段就是清除那些没有被标记的对象。垃圾回收的这个基本算法被称为 “mark-and-sweep”,从根开始遍历标记,没有被遍历过的对象则被清除。
这个过程自动完成,为了不影响代码执行,JS引擎做出的许多优化:
-
分代收集 —— 对象被分成两组:『新的』和『旧的』。许多对象出现,完成他们的工作并快速释放,他们可以很快被清理。那些长期存活下来的对象会变得『老旧』,而且检查的次数也会减少。
-
增量收集 —— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分逐一处理。这需要他们之间额外的标记来追踪变化,但是会有许多微小的延迟而不是大的延迟。
-
闲时收集 —— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
尽量减少垃圾回收
浏览器可以进行垃圾自动回收,但当代码比较复杂时,垃圾回收代价太大。
- 数组:最简单的办法赋值[],但同时创建了一个空对象;可以设置数组长度为0
- 对象:赋值null
- 函数:在循环的函数表达式中,如果可以复用,尽量放在函数的外面
三、内存泄漏
内存泄漏是指计算机可用的内存越来越少,主要是由于程序不能释放那些不再使用的内存。
常见的几种情况:
-
上面👆提到的循环引用
-
闭包使局部变量常驻内存,需要手动清除
-
无意声明的全局变量
function foo(arg) { const bar = ""; } foo(); // 当 foo 函数执行后,变量 bar 就会被标记为可回收。 // 因为当函数执行时,函数创造了一个作用域来让函数里的变量在里面声明。 // 进入这个作用域后,浏览器就会为变量 bar 创建一个内存空间。 // 当这个函数结束后,其所创建的作用域里的变量也会被标记为垃圾,在下一个垃圾回收周期到来时,这些变量将会被回收。
// 改👇 function foo(arg) { bar = ""; } foo(); // 无意中声明了一个全局变量,会得到 window 的引用,bar 实际上是 window.bar, // 它的作用域在 window 上,所以 foo 函数执行结束后,bar 也不会被内存收回。 // 另一种情况 function foo() { this.bar = ""; } foo(); // 函数自调用this 指的是 window,犯的错误跟上面类似,创建了一个全局对象。
-
被遗忘的定时器或回调函数(在别的博客中看到的,还没有遇到过实际场景)
设置了setInterval定时器,但忘记取消,如果循环函数有对外部变量的引用的话,这个变量就会一直存在内存。 -
脱离DOM的引用
获得一个DOM元素的引用,后面这个元素被删除。由于一直保存了这个元素的引用,所以无法被回收。
四、补充:弱引用
在WeakSet和Weak Map中,我第一次了解到弱引用,然后我想着捋一下垃圾回收和内存泄漏这部分的内容,之后就有了这篇文章。
前面我们所提到的引用都是强引用,弱引用一个很特别的存在,就是垃圾回收机制不考虑该对象的引用。
解释一下就是,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还在该弱引用的结构中。
基于弱引用的数据结构:WeakMap、WeakSet、WeakRef。Weak Set中的对象引用是弱引用。WeakMap中的键也是弱引用(值不是)。
🌰 以WeakMap解释一下弱引用
有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1
和e2
占用的内存。一旦忘记手动删除,就会造成内存泄露。请看下面的例子🌰。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
WeakMap
的设计目的在于,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。举个例子🌰
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
🚗 参考资料