【JavaScript】内存管理和垃圾收集机制
师傅给我的模拟面试,感觉我个人对这点没深入吧,再总结学习以下。
简介
像其他的编程语言都有各自底层的内存管理接口,比如 C 语言的 malloc()
和 free()
。但是,JavaScript 是在创建变量时自动分配内存,在不使用时自动释放,这个释放过程称为垃圾回收。
JavaScript 的这种自动释放的机制可以让我们开发者在大部分时间都不需要关心 JavaScript 的内存管理,但话是这么说,我们还是要了解一下滴。
内存的生命周期
大部分编程语言,内存的生命周期都大概分为三步:
- 分配你所需的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放
JavaScript 的内存分配
1. 值的初始化
为了不让咋们前端程序员分配内存,JavaScript 在定义变量时就完成了内存分配。
let n = 123; // 给数值变量分配内存
let s = "string"; // 给字符串分配内存
// 给对象及其包含的值分配内存
let o = {
a: 1,
b: null,
};
// 给数值及其包含的值分配内存(跟对象差不多)
let arr = [1, null, "string", {}];
// 给函数(可调用的对象)分配内存
function f(a) {
return a * 2;
}
// 函数表达式(回调函数)也能分配一个对象
setTimeout(function(){
console.log('callBack fun');
},1000)
2.通过函数调用分配内存
有些函数调用的结果是分配对象内存:
const d = new Date() // 分配一个 Date 对象
const e = document.createElement('div') // 分配一个 DOM 元素对象
有些方法返回的结果分配新变量或者新对象内存:
let s = "string";
let s2 = s.substring(0, 3); // s2 是一个新的字符串
// 因为字符串是不变量
// JavaScript 可能绝对不分配内存
// 只是存储了 [0-3] 的范围
let arr = [1, 2];
let arr2 = [3, 4];
let arr3 = arr.concat(arr2)
// arr 数组有四个元素,是 arr 连接 arr2 的结果
使用值(内存)
使用值的过程实际上是对分配内存进行读取或者写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至是传递函数的参数。
释放内存
当内存不再需要使用时释放内存,但最难的任务是我们要如何找到“哪些被分配的内存确实是已经不再需要了”,如果没有垃圾回收机制,就需要开发者手动去确定程序中那一块内存不再需要了并且主动去释放它。因此,大部分内存管理的问题都在这个阶段。
大部分高级语言都内嵌了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。但还是存在一些内存是无法通过垃圾回收器进行跟踪,因此,上述跟踪也只是一个近似的过程,而不是百分之百。
垃圾回收算法
因为内存无法达到百分百跟踪,因此,垃圾回收实现只能限制的解决一般问题。我们主要需要了解的就是主要的垃圾回收算法和他们的局限性。
引用的概念
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另外一个对象的权限(显示、隐式),叫做一个对象引用另一个对象。比如,JavaScript 的原型链,就是一个对象具有对它原型的引用(隐式引用)和对它属性方法的引用(显示引用)。
在 JavaScript 中,引用“对象”的概念不仅特指 JavaScript 对象,还包括作用域(全局作用域、函数作用域、块级作用域)。
引用计数算法
最初始的垃圾收集算法,算法很简单,就是把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象,对象就会被垃圾收集机制回收。
事例:
let o = {
a: {
b: 2,
},
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集
let o2 = o; // o2 变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有
let oa = o2.a // 引用“这个对象”的 a 属性;现在,“这个对象”有两个引用了,一个是 o2,一个是 oa
o2 = 'my_new_o2' // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了; 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收
oa = null // a 属性的那个对象现在也是零引用了; 它可以被垃圾回收了
但是引用计数存在一个很严重的问题,那就是循环引用。循环引用的情况对象无法被垃圾回收机制回收。比如下面的例子,两个对象被创建,并相互引用,形成了一个循环。他们被调用之后会离开函数作用域,所以他们应该已经没有用到了,理论上应该被回收了。但是,引用计数算法考虑到它们互相之间至少有一次引用,所以他们不会被回收。
function f() {
let o = {};
let o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "success";
}
f();
正是因为上面的限制,早期 IE 浏览器用引用计数的方式对 DOM 对象进行垃圾回收时,经常造成对象被循环引用从而造成内存泄漏问题。
let div;
window.onload = function () {
div = document.getElementById("myDivElement");
div.circularReference = div; // 这里循环引用了
div.lotsOfData = new Array(10000).join("*");
};
标记清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法会假定设置一个叫做根(root)的对象(在 JavaScript 中,根是全局对象 Global)。垃圾回收器将定期聪根开始,找所有从根开始引用的对象,然后找到这些对象引用的对象...以此类推,从根开始,垃圾回收器将找到所有可以获得到的对象和收集所有不能获得的对象。
这个算法比引用计数算法较好,因为在引用计数中,“有零引用的对象”总是不可获取的,但是相反却不一定,参考循环引用;而标记清除因为必须要获得,相互引用的两个对象是无法通过第三方引用获得的。
从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记清除算法的改进,并没有改进标记清除算法本身和它对“对象是否不再需要”的简化定义。
深入我们可以了解下 V8 对 GC 的优化。
V8 对 GC 的优化
待更新...