JavaScript - 垃圾回收及浏览器性能

Posted 友人A ㅤ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript - 垃圾回收及浏览器性能相关的知识,希望对你有一定的参考价值。

1. 垃圾回收

javascript通过自动内存管理实现内存分配闲置资源回收


基本思路: 确定哪个变量不会再使用,就释放它所占用的内存。


特点:

  • 周期性:垃圾回收程序每隔一定时间(或在代码执行过程中某个预定的收集时间)会自动运行
  • 不可判定:垃圾回收过程是一个近似且不完美的方案,因为不确定某块内存是否还有用。这也无法靠算法解决

过程:

如函数中的局部变量。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。

但是,并不是任何时候都能明显看出不再需要这个局部变量的。

因此,垃圾回收程序必须跟踪记录哪个变量还会使用,那个不会再使用,一遍回收内存。


标记策略:

  1. 标记清理
  2. 引用计数

1. 标记清理(常用)

过程:

  1. 变量进入上下文时,该变量会被加上存在于上下文的标记
  2. 只要上下文中的代码在运行,就有可能用到他们,所以此时不会释放他们的内存
  3. 当变量离开上下文时,会被加上离开上下文的标记
  4. 垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

给变量加标记的方式有多种,如:

  1. 当变量进入上下文时,反转给某一位
  2. 维护“在上下文中”和“不在上下文中”两个变量列表,把变量从一个列表转移到另一个列表

2. 引用计数

思路: 对每个值都记录它被引用的次数。

  • 声明变量并给它赋一个引用值时,这个值的引用数为1。
  • 如果同一个值又被赋给另一个变量,那么引用数加1。
  • 类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。
  • 当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。

存在问题: 循环引用。

如对象A有一个指针指向对象B,而对象B也引用了对象A。

// 引用计数策略下,objectA和objectB在函数结束后还会存在,因为它们的引用数永远不会变成0
// 如果函数被多次调用,将导致大量内存无法释放
function problem() {
    // objectA和objectB通过各自的属性相互引用,意味着它们的引用数都是2
    let objectA = new Object();
    let objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}

2. 性能

现象: 垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。


开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。


现代垃圾回收程序会基于对JavaScript运行时环境的探测来决定何时运行。


内存管理

使用垃圾回收的编程环境中开发者通常不需要关心内存管理,但一般分配给浏览器的内存较少,需要避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃的情况。


因此,将内存占用量保持在一个较小的值可以让页面性能更好。


优化内存的最佳手段:保证执行代码时只保存必要的数据。如果数据不必要,则将它设置为null,从而释放其引用。也叫解除引用。适合全局变量和全局对象的属性,因为局部变量超出作用域后会被自动解除引用。

function createPerson(name) {
    // localPerson在createPerson()执行完成超出上下文后会自动被解除引用,不需要显式处理
    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
// 变量globalPerson保存着createPerson()函数调用返回的值
let globalPerson = createPerson('zhangsan');
// globalPerson是一个全局变量,应该在不再需要时手动解除其引用
globalPerson = null;

注意:
解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。


1. 通过 const 和 let 声明提升性能

const和let都以块为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。


2. 隐藏类和删除操作

V8 JavaScript 引擎运行期间会将创建的对象与隐藏类关联起来,以跟踪他们的属性特征。能够共享相同隐藏类的对象性能会更好,V8会针对这种情况进行优化,但不一定总能够做到。如:

function Article() {
    this.title = 'aaa';
}

// V8会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型
let a1 = new Article();
let a2 = new Article();

// 如果添加以下代码,则此时两个Article实例就会对应两个不同的隐藏类
a2.author = 'bbb';

解决方案:

function Article(opt_author) {
    this.title = 'aaa';
    // 避免JavaScript的“先创建再补充”式的动态属性赋值,并在构造函数中一次性声明所有属性
    this.author = opt_author;
}

// 此时两个实例可以共享一个隐藏类,从而带来潜在的性能提升。
let a1 = new Article();
let a2 = new Article('bbb');

使用delete关键字会导致生成相同的隐藏类片段。如:

function Article() {
    this.title = 'aaa';
    this.author = 'bbb';
}

let a1 = new Article();
let a2 = new Article();
delete a1.author;

在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样

function Article() {
    this.title = 'aaa';
    this.author = 'bbb';
}

let a1 = new Article();
let a2 = new Article();
// 最佳实践是把不想要的属性设置为null
// 这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果
a1.author = null;

3. 内存泄漏

在内存有限的设备上,或者在函数会被调用很多次的情况下,存在内存泄露的现象。JavaScript中的内存泄漏大部分是由不合理的引用导致的。


  1. 内存泄漏 — 意外声明全局变量:
function setName() {
	// 解释器会把变量name当作window的属性来创建,只要window本身不被清理就不会消失
	name = 'aaa';
}
  1. 内存泄漏 — 定时器:
let name = 'aaa';
// 定时器的回调通过闭包引用了外部变量,只要定时器一直运行,回调函数中引用的name就会一直占用内存
setInterval(() => {
	console.log(name);
}, 1000);
  1. 内存泄漏 — 闭包:
// 调用outer()会导致分配给name的内存被泄漏
// 代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理name,因为闭包一直在引用着它
let outer = function() {
	let name = 'aaa';
	return function() {
		return name;
	}
}

4. 静态分配与对象池

理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。


浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,就会影响性能。如:

// 调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者

// 如果这个矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值
// 假如这个矢量加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排垃圾回收
function addVector(a, b) {
    let resultant = new Vector();
    resultant.x = a.x + b.x;
    resultant.y = a.y + b.y;
    return resultant;
}

解决方案:不要动态创建矢量对象

function addVector(a, b, resultant) {
    resultant.x = a.x + b.x;
    resultant.y = a.y + b.y;
    return resultant;
}

对象池

在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。

// 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(v1, v2, v3);
console.log([v3.x, v3.y]);

vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);

// 如果对象有属性引用了其他对象,则需要把这些属性设为null
v1 = null;
v2 = null;
v3 = null;

以上是关于JavaScript - 垃圾回收及浏览器性能的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 内存泄漏 及 垃圾回收机制

JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理)

Chrome 浏览器垃圾回收机制与内存泄漏分析(未完成)

javascript的垃圾回收机制和内存管理

JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理V8分代回收Performance使用)

JavaScript 性能优化