4.3 垃圾回收

垃圾回收(Garbage Collection,简称 GC)是 JavaScript 引擎自动管理内存的机制,它会定期找出不再使用的变量和对象,并释放它们占用的内存空间。在 JavaScript 中,开发者不需要手动分配和释放内存,这避免了:内存泄漏、野指针、重复释放、忘记释放内存等问题。

4.3.1 主要垃圾回收算法

1|引用计数(早期算法)

工作原理:跟踪每个值被引用的次数

// 引用计数示例
let obj1 = { name: "John" };    // 引用计数: 1
let obj2 = obj1;                // 引用计数: 2

obj1 = null;                    // 引用计数: 1
obj2 = null;                    // 引用计数: 0 → 可回收

循环引用问题

function problem() {
  let objA = {};
  let objB = {};

  objA.ref = objB;  // objB 被 objA 引用
  objB.ref = objA;  // objA 被 objB 引用

  // 即使函数执行完毕,引用计数都不为0,无法回收
}
2、标记-清除算法(现代主流)

工作原理:从根对象(全局对象)开始,标记所有可达对象,清除未标记对象

// 标记-清除示例
function createObjects() {
  let tempObj = { data: "temporary" };
  let persistentObj = { data: "persistent" };

  window.globalRef = persistentObj;  // 被全局引用
  // tempObj 在函数结束后不可达,将被回收
}

createObjects();

标记过程

  1. 从根对象(window/global)开始
  2. 标记所有可访问的对象
  3. 遍历被标记对象的引用,递归标记
  4. 清除所有未标记的对象

4.3.2 内存管理

出于安全考虑,为了避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃,将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证执行代码时只保存必要的数据,如果数据不再需要,那么将它设置为null,从而释放其引用(解除引用)。这个方式适合全局变量和全局对象的属性,而局部变量会在超出作用域时自动解除引用。

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

ES6增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比var,使用这两个关键字肯可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。

2、内存泄露

JavaScript中的内存泄露大部分是由不合理的引用导致的。常包含下列产生场景:

1 意外的全局变量
// ❌ 错误做法
function leak() {
    leakedVar = '这是一个全局变量';  // 未使用 var/let/const
    this.globalProp = '这也是全局变量';
}

// ✅ 正确做法
function noLeak() {
    let localVar = '这是局部变量';
    const localConst = '这也是局部变量';
}
2 闭包使用不当
// ❌ 可能造成内存泄漏
function createClosure() {
    let largeData = new Array(1000000);
    
    return function() {
        // 即使不需要 largeData,它仍然被保留
        console.log('一些操作');
    };
}

// ✅ 优化版本
function createOptimizedClosure() {
    let largeData = new Array(1000000);
    
    // 使用完成后显式释放
    const result = function() {
        console.log('一些操作');
    };
    
    largeData = null;  // 帮助 GC
    return result;
}
3 定时器和回调函数
// ❌ 未清理的定时器
let data = { content: '大量数据' };
setInterval(() => {
    console.log(data.content);
}, 1000);

// ✅ 正确管理
let timer;
function startProcess() {
    let localData = { content: '数据' };
    timer = setInterval(() => {
        console.log(localData.content);
    }, 1000);
}

function stopProcess() {
    clearInterval(timer);
    // localData 可以被回收
}
4 DOM 引用
// ❌ DOM 引用未清理
let elements = {
  button: document.getElementById('myButton'),
  container: document.getElementById('container')
};

// 即使从 DOM 中移除,JavaScript 引用仍然存在
document.body.removeChild(elements.container);

// ✅ 正确做法
function cleanUp() {
  elements.button = null;
  elements.container = null;
  elements = null;
}
5 Event Listeners
// ❌ 未移除的事件监听器
class Component {
  constructor() {
    this.data = new Array(10000);
    window.addEventListener('resize', this.handleResize.bind(this));
  }

  handleResize() {
    console.log('窗口大小改变');
  }
}

// ✅ 正确管理
class OptimizedComponent {
  constructor() {
    this.data = new Array(10000);
    this.boundHandleResize = this.handleResize.bind(this);
    window.addEventListener('resize', this.boundHandleResize);
  }

  handleResize() {
    console.log('窗口大小改变');
  }

  destroy() {
    window.removeEventListener('resize', this.boundHandleResize);
    this.data = null;
  }
}
3、内存管理最佳实践
1 对象池模式

尤其是在移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。浏览器决定何时运行垃圾回收程序的一个标准就是对象更替速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采取更激进的方式,更频繁地调度垃圾回收程序运行,影响性能。解决办法可以在初始化的一刻,创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象,设置其属性、使用它,然后操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替。

class ObjectPool {
  constructor(createFn) {
    this.createFn = createFn;
    this.pool = [];
  }

  get() {
    return this.pool.length > 0 ? this.pool.pop() : this.createFn();
  }

  release(obj) {
    // 重置对象状态
    Object.keys(obj).forEach(key => {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        obj[key] = null;
      }
    });
    this.pool.push(obj);
  }
}

// 使用示例
const vectorPool = new ObjectPool(() => ({ x: 0, y: 0, z: 0 }));

const v1 = vectorPool.get();
v1.x = 10; v1.y = 20; v1.z = 30;
// 使用完成后...
vectorPool.release(v1);
2 避免内存泄漏的模式
// 使用 WeakMap 和 WeakSet 避免强引用
const weakMap = new WeakMap();
let domElement = document.getElementById('element');

weakMap.set(domElement, { someData: 'data' });

// 当 domElement 被移除时,关联数据会自动被 GC
domElement = null;

// 使用 WeakRef 和 FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} 被垃圾回收了`);
});

let largeObject = { data: new Array(1000000) };
registry.register(largeObject, "大型对象");

largeObject = null;  // 最终会被 GC 回收
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐