js拥有垃圾回收

浏览器自动内存管理实现内存分配和闲置资源回收。

确定哪个变量不再使用,就释放内存。每隔一段时间会自动运行。

垃圾回收的主要策略

标记清理

js最常用的垃圾回收策略。

  • 标记内存中存储的所有变量。
  • 将所有在上下文中的变量和上下文中被变量引用的变量的标记去掉。
  • 之后再被加上标记的变量就是待删除的了。因为任何在上下文中的变量都访问不到他们了。
  • 之后垃圾回收做一次内存清理。

引用计数

没那么常用的垃圾回收策略。

  • 对每个值都记录它被引用的次数。
  • 声明变量并赋一个引用值时,该值引用数为1
  • 同一个值赋予另一个变量,那么引用数+1
  • 如果保存对该值引用的变量被其他值覆盖了,那么引用数-1
  • 当值的引用数为0时,表明无法访问了,可以收回内存了。

但在循环引用的情况下,会出现问题:

  1. function problem() {
  2. let objectA = new Object();
  3. let objectB = new Object();
  4. objectA.someOtherObject = objectB;
  5. objectB.anotherObject = objectA;
  6. }

在标记清理的策略下,函数结束后,两个对象都不在作用域中,因此可以被正常回收。
但在引用计数策略下,两个对象的引用数都是2,因此不会回收,若该函数被调用多次,则会导致大量的内存无法释放。

内存管理

现代浏览器中,通常无需过多关心内存管理,但是还是可以优化。

解除引用

执行代码时只保存必要数据,若数据不再必要,将其设置为 null,释放其引用。也叫作 解除引用。
适合全局变量和全局对象属性。因为局部变量超出作用域后会被自动解除引用。

  1. function createPerson(name){
  2. let localPerson = new Object();
  3. localPerson.name = name;
  4. return localPerson;
  5. }
  6. let globalPerson = createPerson("Nicholas");
  7. // 解除 globalPerson 对值的引用
  8. globalPerson = null;

localPerson 变量在函数执行完后会自动解除引用,因为超出作用域了。
但是globalPerson对其进行了引用,且是个全局变量,一直处于全局上下文中,应该在不要时手动解除其引用。

但是解除一个值的引用并不会确保内存被回收。关键在于确保相关的值已经不在上下文中了。

1. 使用 let、const

let、const会产生块级作用域。相比于使用var,这两个关键字更可能尽早触发垃圾回收。

2. 隐藏类和删除操作

V8引擎会利用隐藏类做优化:

  1. function Article() {
  2. this.title = 'Inauguration Ceremony Features Kazoo Band';
  3. }
  4. let a1 = new Article();
  5. let a2 = new Article();

V8 会给上述代码的两个类实例共享同一个隐藏类。因为他们源于同一个构造函数和原型。
假设之后又添加了:a2.author = 'Jack'

此时由于a2 多了一个属性,a1、a2 将不再共享一个隐藏类,而是使用两个隐藏类。根据操作频率和类的大小,有可能对性能产生明显影响。

解决方案就是,避免“先创建再补充”,而是一次性在构造函数中声明所有属性(不考虑 hasOwnProperty 返回值的情况下):

  1. function Article(opt_author) {
  2. this.title = 'Inauguration Ceremony Features Kazoo Band';
  3. this.author = opt_author;
  4. }
  5. let a1 = new Article();
  6. let a2 = new Article('Jake');

注意不要使用 delete 来删除,这样也会导致生成多个隐藏类。不想要的属性值可以设置为 null

3. 内存泄漏

意外声明全局变量:

  1. function setName() {
  2. name = 'Jake'; // 此时name直接被挂载在全局上下文 window 对象上。
  3. }

定时器:

  1. let name = 'Jake';
  2. setInterval(() => {
  3. console.log(name);
  4. }, 100);

闭包:

  1. let outer = function() {
  2. let name = 'Jake';
  3. return function() {
  4. return name;
  5. };
  6. };

只要闭包返回的函数存在,就无法清理name变量,因为闭包一直在引用。只能解除引用。

4. 静态分配与对象池

垃圾回收的运行也是要占用资源的。合理使用分配的内存,减少垃圾回收次数。

  1. function addVector(a, b) {
  2. let resultant = new Vector();
  3. resultant.x = a.x + b.x;
  4. resultant.y = a.y + b.y;
  5. return resultant;
  6. }

上述代码会创建一个新对象。上下文结束后回收 resultant 的内存。但如果有很多次的调用该函数,就会不断的触发垃圾回收机制。
该问题的解决方案是不要动态创建矢量对象:

  1. let resultant = new Vector();
  2. function addVector(a, b, resultant) {
  3. resultant.x = a.x + b.x;
  4. resultant.y = a.y + b.y;
  5. return resultant;
  6. }

这样我们从始至终只需要一个对象,不会频繁的触发垃圾回收机制。

如何创建这种对象池?

  1. // vectorPool 是已有的对象池
  2. let v1 = vectorPool.allocate();
  3. let v2 = vectorPool.allocate();
  4. let v3 = vectorPool.allocate();
  5. v1.x = 10;
  6. v1.y = 5;
  7. v2.x = -3;
  8. v2.y = -6;
  9. addVector(v1, v2, v3);
  10. console.log([v3.x, v3.y]); // [7, -1]
  11. vectorPool.free(v1);
  12. vectorPool.free(v2);
  13. vectorPool.free(v3);
  14. // 如果对象有属性引用了其他对象
  15. // 则这里也需要把这些属性设置为 null
  16. v1 = null;
  17. v2 = null;
  18. v3 = null;

大部分情况下,静态分配都是优化的极端形式。除非你的程序被垃圾回收严重拖了后腿。大部分情况,都属于过早优化,不用考虑。