了解Javascript的垃圾回收机制以及相关的性能优化

7/24/2021 垃圾回收

# 了解Javascript的垃圾回收机制以及相关的性能优化

Javascript是使用垃圾回收的语言,与C和C++不同,Javascript通过自动内存管理实现了内存分配和闲置资源回收。

  1. # Javascript垃圾回收的机制概念

很多前端面试都会问到JS的垃圾回收机制。其实JS语言的开发者当初的设计思路很简单:定期审视所有内存中的变量,确定哪些变量不会再使用,就释放掉它占用的内存。

  1. # 垃圾回收的策略

垃圾回收主要的工作其实就是标记哪些变量还会继续使用,哪些不再使用;在浏览器发展的历史长河中,主要用到过两种标记策略:标记清理引用计数

# 标记清理

标记清理是当前Javascript的主流垃圾回收策略,其方法如下:

  • 当一个变量a进入了执行上下文A,那么这个变量a会被打上一个存在于上下文的标记;只要执行上下文A没有结束,应该永远不会释放变量a,因为此上下文有可能用到它;
  • 当执行上下文A结束,变量a会被打上一个离开上下文的标记;
  • 垃圾回收程序运行的时候,会标记内存中存储的所有变量;
  • 将所有存在于上下文标记的变量以及被存在于上下文标记的变量引用的变量的标记去掉(比较拗口);
  • 剩下被标记的变量即是准备删除的变量;
  • 垃圾回收程序做一次内存清理,销毁带标记的所有变量的值并收回它们的内存;

# 引用计数

引用计数是早期浏览器使用的垃圾回收策略。它的思路是对每个变量都加一个值记录被引用次数。其中

  • 第一次声明并赋值时,次数标为1;
  • 每次被赋值给其他变量次数+1;
  • 如果保存对该值引用的变量被其他值覆盖,则次数-1;
  • 当次数为0时,说明这个值被其他变量引用次数为0,没法再访问到这个值了(除非你知道这个值在栈内存里的地址)
  • 垃圾回收程序将次数为0的值的内存释放;

引用计数存在一个很大的问题:循环引用。如果存在两个对象A和B,其中对象A的一个指针指向B,而对象B的一个指针指向A,那这就是循环引用。比如:

function issue(){
    let objA = new Object();
    let objB = new Object();
    objA.prop1 = objB;
    objB.prop2 = objA;
}
1
2
3
4
5
6

可以看到,两个对象通过自己的属性相互引用,那他们俩的引用数永远是2,且永远不会变为0,这就会导致大量内存占用;

  1. # 垃圾回收相关的性能优化

# 3.1 对于代码中不再用的变量手动解除引用

内存占用的越小,页面的性能就越好。对于代码中不再用的变量,把它设置为null,从而手动接触引用。例如:

function Person(name){
    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
let gobalPerson = Person("will.wei")
/* *
 * 业务代码
 * */
//解除引用
globalPerson = null;
1
2
3
4
5
6
7
8
9
10
11

注意:解除引用并不会触发相关内存被回收,接触引用的关键在于确保不需要的值已经不存在于上下文中,那么它在下次垃圾回收时就会被回收。

# 3.2 使用const和let,避免使用var来声明变量

constlet都是以块为作用域,所以使用这两个新关键字,能让垃圾回收程序更早介入,尽早收回内存。当然,constlet不仅仅只有这一点好处,关于这两个变量,有时间会好好总结一下,单独写一篇博客。

# 3.3 避免隐藏类和删除操作

Chrome浏览器使用的是V8引擎。运行期间,V8会将创建的对象与隐藏类关联起来,对于属性一致的,使用共同隐藏类,对于属性不一致的,会使用不同的隐藏类。例如:

function Person(){
    this.name = 'will.wei';
}
let p1 = new Person();
let p2 = new Person();
//此时V8引擎会在后台配置使p1和p2使用相同的隐藏类,因为这两个实例共享一个构造函数和原型

p2.age = '18'
//此时这两个实例会对应两个不同的隐藏类,这是V8引擎导致的
1
2
3
4
5
6
7
8
9

所以,要避免Javscript的“先创建再补充”式的动态属性赋值,并在构造函数中一次性声明所有的属性。
其次,delete方法和动态添加属性的效果一样,更好的方式是把不想要的属性设置为null

# 3.4 避免意外声明全局变量以及闭包引起的内存泄露

  • 意外声明全局变量:
function setNumber(){
    count = 0;
}
1
2
3

这里面因为count未声明,解释器会把count当作window的属性。在给变量赋值时确认用const或let来声明。

  • 闭包引起的内存泄露:
let name = 'will.wei';
setInterval(()=>{
    console.log(name);
},100)
1
2
3
4

这里定时器的回调通过闭包使得引用name这个变量一直存在,那么name的内存就永远不会被回收

# 3.5 对于经常使用的函数,避免在函数内new 新的对象

function Person(a,b){
    let name = new Name();
    name.lastName = a;
    name.firstName = b;
    return name;
}
1
2
3
4
5
6

当有很多对象被初始化时,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序。
如上面的那个函数,如果频繁调用该函数,那么会在堆上创建一个Name对象,然后修改它,最后再把它返给调用者,如果调用的声明周期很短,那么会频繁地安排垃圾回收。
解决方案:避免在函数内new 新的对象,可以让它使用一个已有的对象:

function Person(a,b,name){
    name.lastName = a;
    name.firstName = b;
    return name;
}
1
2
3
4
5