js拥有垃圾回收
浏览器自动内存管理实现内存分配和闲置资源回收。
垃圾回收的主要策略
标记清理
js最常用的垃圾回收策略。
- 标记内存中存储的所有变量。
- 将所有在上下文中的变量和上下文中被变量引用的变量的标记去掉。
- 之后再被加上标记的变量就是待删除的了。因为任何在上下文中的变量都访问不到他们了。
- 之后垃圾回收做一次内存清理。
引用计数
没那么常用的垃圾回收策略。
- 对每个值都记录它被引用的次数。
- 声明变量并赋一个引用值时,该值引用数为1
- 同一个值赋予另一个变量,那么引用数+1
- 如果保存对该值引用的变量被其他值覆盖了,那么引用数-1
- 当值的引用数为0时,表明无法访问了,可以收回内存了。
但在循环引用的情况下,会出现问题:
function problem() {let objectA = new Object();let objectB = new Object();objectA.someOtherObject = objectB;objectB.anotherObject = objectA;}
在标记清理的策略下,函数结束后,两个对象都不在作用域中,因此可以被正常回收。
但在引用计数策略下,两个对象的引用数都是2,因此不会回收,若该函数被调用多次,则会导致大量的内存无法释放。
内存管理
解除引用
执行代码时只保存必要数据,若数据不再必要,将其设置为 null,释放其引用。也叫作 解除引用。
适合全局变量和全局对象属性。因为局部变量超出作用域后会被自动解除引用。
function createPerson(name){let localPerson = new Object();localPerson.name = name;return localPerson;}let globalPerson = createPerson("Nicholas");// 解除 globalPerson 对值的引用globalPerson = null;
localPerson 变量在函数执行完后会自动解除引用,因为超出作用域了。
但是globalPerson对其进行了引用,且是个全局变量,一直处于全局上下文中,应该在不要时手动解除其引用。
但是解除一个值的引用并不会确保内存被回收。关键在于确保相关的值已经不在上下文中了。
1. 使用 let、const
let、const会产生块级作用域。相比于使用var,这两个关键字更可能尽早触发垃圾回收。
2. 隐藏类和删除操作
V8引擎会利用隐藏类做优化:
function Article() {this.title = 'Inauguration Ceremony Features Kazoo Band';}let a1 = new Article();let a2 = new Article();
V8 会给上述代码的两个类实例共享同一个隐藏类。因为他们源于同一个构造函数和原型。
假设之后又添加了:a2.author = 'Jack' 。
此时由于a2 多了一个属性,a1、a2 将不再共享一个隐藏类,而是使用两个隐藏类。根据操作频率和类的大小,有可能对性能产生明显影响。
解决方案就是,避免“先创建再补充”,而是一次性在构造函数中声明所有属性(不考虑 hasOwnProperty 返回值的情况下):
function Article(opt_author) {this.title = 'Inauguration Ceremony Features Kazoo Band';this.author = opt_author;}let a1 = new Article();let a2 = new Article('Jake');
注意不要使用 delete 来删除,这样也会导致生成多个隐藏类。不想要的属性值可以设置为 null 。
3. 内存泄漏
意外声明全局变量:
function setName() {name = 'Jake'; // 此时name直接被挂载在全局上下文 window 对象上。}
定时器:
let name = 'Jake';setInterval(() => {console.log(name);}, 100);
闭包:
let outer = function() {let name = 'Jake';return function() {return name;};};
只要闭包返回的函数存在,就无法清理name变量,因为闭包一直在引用。只能解除引用。
4. 静态分配与对象池
垃圾回收的运行也是要占用资源的。合理使用分配的内存,减少垃圾回收次数。
function addVector(a, b) {let resultant = new Vector();resultant.x = a.x + b.x;resultant.y = a.y + b.y;return resultant;}
上述代码会创建一个新对象。上下文结束后回收 resultant 的内存。但如果有很多次的调用该函数,就会不断的触发垃圾回收机制。
该问题的解决方案是不要动态创建矢量对象:
let resultant = new Vector();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]); // [7, -1]vectorPool.free(v1);vectorPool.free(v2);vectorPool.free(v3);// 如果对象有属性引用了其他对象// 则这里也需要把这些属性设置为 nullv1 = null;v2 = null;v3 = null;
大部分情况下,静态分配都是优化的极端形式。除非你的程序被垃圾回收严重拖了后腿。大部分情况,都属于过早优化,不用考虑。
