本文目标:
- 学习红宝书4.3节中提到的JavaScript垃圾回收常用策略
- 学习红宝书4.3节中提到的内存管理知识
1. 垃圾回收是什么
在C和C++的语言中,如何管理内存是一个很大的负担,但JavaScript语言为开发者卸下了重担,开发者不需要主动去执行垃圾回收的策略,我们也无法手动触发这个操作。
垃圾回收是确定哪一块变量不会再使用,然后释放它的内存。如何确定一个变量是否再使用呢?主要是两个算法,标记整理和引用计数
2. 标记整理
JS红宝书是这样描述的:
当变量进入上下文时,比如在函数内部声明一个变量时,这个变量会被加上'存在于上下文的标记'。而不在上下文中的变量,逻辑上讲永远不应该释放他们的内存,因为只要上下文中的代码在运行,就有可能用到他们、
首先,这段描述我是有点不完全懂的。我能理解‘如果函数内部声明一个变量,变量会被加上标记’这个地方。不在上下文,我理解的是,如果变量不在函数内,在全局,逻辑上来看,不应该释放他们的内存,因为其他函数都有可能用到全局的变量。全局变量永远不应该释放,他在根对象是可达的。
书中描述:
垃圾回收机制运行的时候,会标记内存中所有的变量:
- 将所有上下文中的变量和'被上下文中的变量引用的变量'的标记去掉
- 之后再被加上标记的变量,就是待删除的变量
- 垃圾回收机制随后做一次标记清理,销毁待标记的所有值
3. 引用计数
该方法就是针对每个变量,记录他被引用的次数。当一个值的引用次数为0的时候,就说明没有办法再访问到这个值了,就可以安全的回收他的内存。
如下a变量就是被引用了2次
let a = 1;
function fn () {
console.log(a + 1);
console.log(a + 2);
}
该方法现今不常用,是因为会造成循环引用的问题,
function fn () {
let objA = new Object()
let objB = new Object()
objA.element = objB
objB.element = objA
}
上面代码中,若采用引用计数法,objA和objB永远不会被删除,因为他们的引用数都是2。如果是标记清理法,就没有关系。
书中提到:
在IE8及更早的版本,BOM和DOM对象是通过C++实现的组件对象模型COM,而COM实现引用计数法,JS实现标记清理法,就会出现混用。在IE9,BOM和DOM都改为使用标记整理了,不用引用计数了
避免循环引用需要及时清理变量:
objA = null;
objB = null;
4. 性能
书中提到:
垃圾回收机制会周期性运行,如果分配过多变量,则可能造成性能损失,因此垃圾回收机制的调度很重要。尤其在内存有限的移动设备上,垃圾回收机制可能会明显拖慢渲染的速度和帧速率。 …… IE曾经是根据分配数,分配了256个变量、4096个对象就开始执行垃圾回收机制,后来这样饱受诟病,改为了动态分配变量,如果回收的变量不达内存的15%,则变量、字面量等的阈值就会翻倍。
垃圾回收机制是一个耗性能的操作,不能太过于频繁的执行。但是这里还未指出在代码层面我们如何去进行优化,进而提高回收垃圾的效率,且看下面。
5. 内存管理
5.1 不用就置空
如果一个变量全局变量不怎么使用了,就让其=null,减少内存占用量能够让页面性能更好
function createPerson (name) {
let localPerson = new Object();
localPerson.name = name;
return localPerson;
}
let globalPerson = createPerson('Mike');
globalPerson = null;
如上代码中,局部变量localPerson自动会清空的,globalPerson变量是全局变量,不用了要置空,这样能够断开引用关系。但是垃圾回收机制并不会在置空时立刻执行。
5.2 使用const和let关键字声明变量
使用let和const而不是var,可能 能够让垃圾回收机制更早的介入
5.3 隐藏类和删除操作
隐藏类是JavaScript引擎(如V8)中用于优化对象属性访问性能的一种机制,该机制能帮助JS引擎快速找到对象的内存地址。在代码运行期间,V8会将创建的对象和隐藏类关联起来,以跟踪他们的属性特征(属性偏移量)。在这里这个概念可能还不是很明晰,请接着往下看,
如下,a1和a2的实例会共享相同的隐藏类(这个隐藏类注意是一个机制),因为他们共享同一个构造函数:
function Article () {
this.title = 'aaaaaaaaa'
}
let a1 = new Article ()
let a2 = new Article()
如果执行a1增加了一个属性,或者执行删除a2的属性,都会让这两个实例对应不同的隐藏类。
a1.author = '罗贯中'
delete a2.author
这样的操作频率越大,或者隐藏类越大,都会对性能产生很大的影响。
更好的做法如下,要在构造函数中就增加好共有的属性,某个实例的对象不需要该属性就置为null,这样他们共享的就是同样的隐藏类
function Article () {
this.title = 'aaaaaaaaa'
+ this.author = '罗贯中'
}
let a1 = new Article ()
let a2 = new Article()
+ a2.author = null
6. 内存泄漏
内存泄漏是变量不再被使用本来应该被释放了,但是因为不合理的引用导致没有被释放,占用了一定的内存,甚至导致内存暴涨,进而程序崩溃。
全局变量
name变量,没有使用任何关键字声明,结果会变成全局变量,成为window上面的变量,只要window不销毁,变量就会一直在。
function fn () {
name = '333'
}
定时器
定时器引用变量,也会造成内存泄漏,如下延时器通过闭包访问到name变量,就一直不被销毁
function fn() {
let name = 123;
setInterval(() => {
console.log(name, 'name')
})
}
闭包
fn2函数返回了闭包函数,在外部执行了,执行时会一直访问内部的name变量,进而造成内存泄漏
function fn2 () {
let name = 123;
return function fn3 () {
return name
}
}
let outer = fn2()
outer()
7. 静态分配与对象池
浏览器决定垃圾回收程序的标准就是对象更替的速度,如果有很多对象被初始化,然后一下子又都超出了作用域,浏览器会采用更加激进的方式去调度垃圾回收机制运行,这样就会影响性能。
如下代码中,该函数创建一个矢量对象,并返回他,如果该函数被频繁调用,垃圾回收机制就会频繁运行
function addVector (a, b) {
let obj = new Vector();
obj.x = a.x + b.x;
obj.y = a.y + b.y;
return resolution;
}
如果采用对象池的设计模式来进行改造:
- 首先,声明对象池和矢量构造函数
// Vector 构造函数
function Vector (x = 0, y = 0) {
this.x = x;
this.y = y;
}
// 对象池构造函数
function VectorPool () {
this.pool = [] // 存储可以重用的Vector对象
}
- 其次,增加对象池的分配和放回方法
// 从对象池中分配一个Vector对象
VectorPool.prototype.allocate = function () {
// 如果对象池有对象,直接取出返回
if (this.pool.length > 0) {
return this.pool.pop()
}
// 如果没有,则创建一个
return new Vector()
}
// 从对象池中分配一个Vector对象
VectorPool.prototype.free = function (vector) {
this.pool.push(vector)
}
- 增加矢量变量方法
function addVector2 (a, b, resultant) {
resultant.x = a.x + b.x;
resultant.y = a.y + b.y;
return resultant;
}
- 创建对象池,并拿到变量
const vectorPool = new VectorPool()
// vectorPool是已有的对象池
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate();
let v3 = vectorPool.allocate();
v1.x = 10;
v1.y = 5;
v2.x = -3;
v2.y = -6;
- 调用addVector方法,
addVector2(v1, v2, v3)
console.log([v3.x, v3.y], '[v3.x, v3.y]');
// 如果不用了就还给vectorPool的对象池属性
vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);
将矢量变量还给对象池之后的效果
- 将对象置空
v1 = null;
v2 = null;
v3 = null;
这样之后,下次还需要用,再从对象池里面allocate新的对象。
此外,在上面的例子中,对象池的构造函数里面有pools数组,在声明这个数组时,最好能够初始化就知道自己大概要用到多少个数组,如果初始化长度为200不够要增加1个,那么JS引擎会删掉200的长度,再创建201个,垃圾回收机制又会来运行了
function VectorPool () {
this.pool = new Array(201) // 给出指定的长度
}