JavaScript-垃圾回收与性能优化
垃圾回收
JavaScript 是使用垃圾回收的语言,也就是说 执行环境负责在代码执行时管理内存 。
当我们声明一个变量时,它会占用系统的内存。当我们不需要使用该变量时,该变量就成了“垃圾”,我们应该释放该变量所占用的内存,以优化网页的性能。
垃圾回收的基本思路就是,确定哪个变量不再使用了,就释放该变量所占的内存。这个过程是周期性的,比方说以某段时间为一个周期,或者当变量数量或占用的内存到达一定数值时,自动清理无用的变量,释放内存。
虽然 JavaScript 会自动回收“垃圾”,但这并不是完美的。可能存在某些无法被 JavaScript 识别的“垃圾”,这些“垃圾”大多是由程序员无意造成的,需要手动标记清理。
在浏览器的发展史上,主要有两种垃圾回收策略: 标记清理 、 引用计数 。
标记清理
JavaScript 最常用的垃圾回收策略就是标记清理(mark-and-sweep)。
例如当变量进入上下文时,表明该变量正在使用,我们可以对它进行标记。标记的方法有很多,例如将它存入一个“正在使用”的数组。当变量离开上下文时,就表示该变量已经无用了,可以被销毁了,我们可以将它从“正在使用”的数组转移到“无用”的数组。当条件到达时,垃圾回收机制就会自动清理“无用”数组里的变量,并回收他们的内存。
引用计数
另一种策略就是引用计数(reference counting),并不常用。因为它无法识别出值比较“特别”的变量,导致无法回收内存。
它的思路就是对每个值都记录它被变量引用的次数。当一个变量被赋予一个引用类型的值时,那么该值的引用次数就 +1,若该变量被重新赋予一个新的值,切断了跟该值的引用关系,那么该值的引用次数就 -1。当该值的引用数量为 0 时,就表示该值可以被安全的回收了。
看似很合理,但是出现了一个严重的问题,就是引用计数无法处理 循环引用 的情况。
所谓的循环引用就是两个引用类型的值相互引用,如下代码:
const obj1 = new Object(); // 该 引用值1 被 obj1 引用 1次
const obj2 = new Object(); // 该 引用值2 被 obj2 引用 1 次
obj1.add = obj2; // 引用值1 的 add 属性引用了 引用值2,导致 引用值2 的引用次数为 2
obj2.add = obj1; // 引用值2 的 add 属性引用了 引用值1,导致 引用值1 的引用次数为 2
obj1 = null; // 变量 obj1 被重新赋值,切断了跟 引用值1 的联系,引用值1 的引用次数 -1,为 1
obj2 = null; // 变量 obj2 被重新赋值,切断了跟 引用值2 的联系,引用值2 的引用次数 -1,为 1
// 所以之前的引用值的引用次数都不为 0,无法正常回收内存
如果想切断变量与引用值之间的联系,就把变量设置为 null 即可。垃圾回收机制会自动清理值为 null 的变量。
obj1.add = null;
obj2.add = null;
// 像这样 引用值1 和 引用值2 的引用次数就为 0 了
性能优化
如果内存中存在许多变量,会影响程序的性能,所以需要靠垃圾回收机制来清理无用的变量。但是过于频繁的触发垃圾回收机制,不仅不能达到提升性能的目的,反而会造成反作用。所以垃圾回收机制的调度时机十分的重要。
当然垃圾回收的调度时机不是我们能决定的,是浏览器决定的。但是我们可以通过优化代码来配合垃圾回收,从而提升程序性能。
一般情况下,系统分配给浏览器的内存通常会比桌面程序少很多,如果是移动设备的浏览器,能分配到的内存就更少了。所以我们要保持内存的占用量在一个较小的范围。
优化内存占用最佳的手段就是保证执行代码时只保存必要的数据,无用的数据可以赋值为 null 解除引用 ,从而释放内存。
例如:
let obj = {
username: "王五",
password: "1234"
};
obj = null; // 解除引用关系,释放内存
使用 let & const
let 和 const 是 ES6 新增的关键字,用于声明变量。
因为 let 和 const 都有块作用域,所以相比 var ,在某些场景下它们可以更早的被垃圾回收机制发现。例如,在块作用域比函数作用域更早终止的情况下。
多使用 const ,其次 let ,尽量不要使用 var ,更不要不声明就使用变量(因为该变量会提升为全局变量)。
共享隐藏类(V8)
Chrome 浏览器 V8 JavaScript 引擎将代码编译成实际的机器码时会使用“隐藏类”。
运行期间 V8 会将创建的对象与隐藏类关联起来,能够共享相同隐藏类的对象性能会更好。
例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三", 18);
let p2 = new Person("李四", 20);
根据以上代码,V8 会在后台配置,让 p1 和 p2 这两个实例共享相同的隐藏类。
动态添加或删除属性会导致不再共享相同的隐藏类,而是生成各自的隐藏类。
例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三", 18);
let p2 = new Person("李四", 20);
p2.gender = 0; // 动态添加属性,应该避免!
或者
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三", 18);
let p2 = new Person("李四", 20);
delete p2.age; // 动态删除属性,应该避免!
所以我们要避险先创建实例,在补充属性的写法,应该在构造函数里一次性声明完所有的属性。
例如:
function Person(name, age, gender=0) {
this.name = name;
this.age = age;
this.gender = gender;
}
let p1 = new Person("张三", 18);
let p2 = new Person("李四", 20, 0);
也不要使用 delete 表达式动态删除实例的属性。如果不需要该属性,可以给该属性赋予 null。这样既可以保持隐藏类不变并继续共享,又可以让无用的值被回收。
例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三", 18);
let p2 = new Person("李四", 20);
p2.age = null; // 将不用的属性赋值为 null
防止内存泄漏
不好的 JavaScript 编码习惯可以能会出现难以察觉且有害的内存泄漏问题。JavaScript 中的内存泄漏大部分是不合理的引用造成的。
- 意外的全局变量
- 忘记关闭定时器
- 不合理的闭包
意外的全局变量
在不使用关键字声明就使用变量,那么改变了会被提升为全局变量。
function getName() {
myName = "王五"; // myName 未声明就使用!!!
}
解决方法就是给 myName 前面加上 const、let 或 var 关键字。
忘记关闭定时器
忘记关闭定时器也会悄悄泄露内存。
function run() {
setIntervarl(() => {
console.log("光阴似箭,日月如梭");
}, 1000);
}
run();
解决方法就是在不需要定时器的时候关闭即可。
let timer;
function run() {
timer = setIntervarl(() => {
console.log("光阴似箭,日月如梭");
}, 1000);
}
function stop() {
clearInterval(timer); // 关闭定时器
}
run(); // 开启定时器
stop(); // 关闭定时器
不合理的闭包
JavaScript 的闭包很容易不知不觉就造成内存泄漏。
function func1() {
let a = "123";
return function func2() {
return a;
}
}
let result = f1(); // 此时 result 引用了 func2 ,func2 引用了 func1 里的变量 a
// 导致 func1 里的变量 a 无法被回收
// 假设 a 里不是 "123" 而是内容很大的一个字符串,那就可能引发大问题了