背景
JavaScript是使用垃圾回收的语言,执行环境会负责在代码执行时管理内存,通实现自动内存分配和闲置资源回收。垃圾回收是一个周期性的过程,垃圾回收程序每隔一段时间就会自动运行,找到不会再被使用的变量,将其内存释放掉。垃圾回收机制不是一个完美的内存管理方案,因为某块内存是否还有用属于不可判定的问题,无法单靠算法解决。以下介绍Js中主要用到的两种垃圾回收策略:标记清除和引用计数
标记清理
Js最常用的垃圾回收算法为标记清理算法,主要步骤为:
- 通过GC标记内存中存储的所有变量
- 将所有在上下文中的变量以及在上下文中的变量引用的变量的标记去掉
- 将仍然被标记的变量的内存回收
- 等待下一轮垃圾回收
局限性:
- 内存碎片化,每轮垃圾回收之后可能会产生内存碎片(空闲的内存空间不连续),如果之后想申请一块大的内存空间,可能找不到合适大小的内存快
- 性能较差,由于内存碎片的存在,分配较大的内存时可能要遍历大半个内存空间,造成了性能的损耗。
引用计数
引用计数算法在Js中没那么常用,它的策略是:
- 声明变量并赋给它一个引用值时,该值的引用数加一
- 保存该引用值的变量被其他值覆盖了,该值的引用数减一
- 周期性清除引用值为0的变量
局限性:
- 循环引用问题,如果两个变量互相引用,那么即使它们离开了自己的作用域,也始终不会被清除
// 例子来自参考资料1
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
/**
在这个例子中,objectA与objectB互相引用,在引用计数算法下,在problem函数结束后它们也不会被清除。如果problem函数被大量调用,就会产生严重的内存损耗。
**/
- 计数器内存占用,如果内存中有很多需要标记的变量存在,那么计数器程序同样会产生大量内存开销。
开发者内存管理
- 对全局变量和全局对象手动解除引用
let value = new Object();
let reference = value
reference = null; // 通过标记为null释放其引用
-
通过const和let声明提升性能
通过const和let声明的变量都以块为作用域,所有使用这两个关键字可以让垃圾变量更早的被回收。 -
对于JavaScript对象,尽量避免“先创建再赋值”
V8引擎在将解释后的JavaScript代码编译为机器码时会利用“隐藏类”,在运行期间,隐藏类会和类实例关联起来,跟踪他们的属性特征。所以为了减少开销,尽量使多个类实例能够共享隐藏类。
// 例子来自参考资料1
function Article() {
this.title = 'Just a title';
}
let a1 = new Article();
let a2 = new Article();
// 此时a1和a2共享一个隐藏类
a2.author = 'Jake';
// 此时a1和a2对应两个隐藏类
// 解决方法
function Article(opt_author) {
this.title = 'Just a title';
this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake');
// 此时a1和a2共享一个隐藏类
// 注意,动态删除属性也会和动态添加属性导致一样的后果
delete a1.author; // a1和a2不再共享一个隐藏类
// 可以通过将属性设置为null来代替delete
a1.author = null; // a1和a2仍共享一个隐藏类
参考资料
- JavaScript高级程序设计(第4版)
- 「硬核JS」你真的了解垃圾回收机制吗
- 一起来看Javascript的垃圾回收机制