icon
垃圾回收

垃圾回收

垃圾回收(Garbage Collection,GC)是 JavaScript 引擎自动管理内存的机制,负责回收不再使用的对象占用的内存。

基本概念

JavaScript 使用自动垃圾回收机制,开发者不需要手动管理内存。当对象不再被引用时,垃圾回收器会自动回收其占用的内存。

function createObject() {
  const obj = { name: 'Alice' };
  return obj;
}

const myObj = createObject();
// obj 在函数执行完毕后,如果没有被引用,会被回收

标记清除算法(Mark-and-Sweep)

这是现代 JavaScript 引擎最常用的垃圾回收算法。

工作原理:

  1. 从根对象(全局对象、当前执行上下文)开始标记所有可达对象
  2. 遍历所有对象,清除未被标记的对象
  3. 回收被清除对象的内存
// 示例
let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };

obj1.ref = obj2;
obj2.ref = obj1;

obj1 = null;
obj2 = null;
// 两个对象形成循环引用,但标记清除算法可以处理

引用计数算法(Reference Counting)

早期浏览器使用的算法,现在已很少使用。

工作原理:

  • 跟踪每个对象被引用的次数
  • 当引用计数为 0 时,立即回收对象

问题:

  • 无法处理循环引用
// 循环引用问题
function createCycle() {
  const obj1 = {};
  const obj2 = {};
  
  obj1.ref = obj2;
  obj2.ref = obj1; // 循环引用
  
  return obj1;
}

const cycle = createCycle();
// 引用计数算法无法回收这两个对象

内存泄漏

内存泄漏是指不再使用的内存没有被正确释放。

  1. 意外的全局变量
// 不好的做法
function leak() {
  name = 'Alice'; // 创建全局变量
  this.age = 25;  // 在非严格模式下,this 指向全局对象
}

// 好的做法
function noLeak() {
  const name = 'Alice';
  const age = 25;
}
  1. 未清理的定时器
// 内存泄漏
function startTimer() {
  setInterval(() => {
    console.log('tick');
  }, 1000);
}

// 正确的做法
let timerId;
function startTimer() {
  timerId = setInterval(() => {
    console.log('tick');
  }, 1000);
}

function stopTimer() {
  clearInterval(timerId);
}
  1. 未清理的事件监听器
// 内存泄漏
function attachListener() {
  const button = document.getElementById('button');
  button.addEventListener('click', function() {
    console.log('clicked');
  });
  // 如果 button 被移除,监听器仍然存在
}

// 正确的做法
function attachListener() {
  const button = document.getElementById('button');
  const handler = function() {
    console.log('clicked');
  };
  button.addEventListener('click', handler);
  
  // 在适当时机移除
  button.removeEventListener('click', handler);
}
  1. 闭包引用
// 可能导致内存泄漏
function createHandler() {
  const largeData = new Array(1000000).fill(0);
  
  return function() {
    // 闭包持有 largeData 的引用
    console.log('handler');
  };
}

const handler = createHandler();
// largeData 不会被回收,因为被闭包引用

// 正确的做法
function createHandler() {
  return function() {
    console.log('handler');
  };
  // largeData 在函数执行完毕后可以被回收
}
  1. DOM 引用
// 内存泄漏
const elements = [];
function storeElements() {
  const divs = document.querySelectorAll('div');
  elements.push(...divs); // 保存 DOM 引用
}

// 即使 DOM 元素被移除,elements 数组仍然持有引用

// 正确的做法
const elementIds = [];
function storeElementIds() {
  const divs = document.querySelectorAll('div');
  divs.forEach(div => {
    elementIds.push(div.id); // 只保存 ID,不保存 DOM 引用
  });
}

V8 引擎的垃圾回收

V8 使用分代垃圾回收策略:

  1. 新生代(New Space)

    • 存储新创建的对象
    • 使用 Scavenge 算法(复制算法)
    • 回收频繁,速度快
  2. 老生代(Old Space)

    • 存储存活时间较长的对象
    • 使用标记清除和标记整理算法
    • 回收较慢,但更彻底
// 新创建的对象在新生代
const obj = { name: 'Alice' };

// 经过多次垃圾回收后,存活的对象会晋升到老生代

优化建议

  1. 及时解除引用
let largeObject = createLargeObject();
// 使用完毕后
largeObject = null; // 帮助垃圾回收
  1. 避免创建不必要的对象
// 不好的做法
function processData(data) {
  return data.map(item => {
    return {
      id: item.id,
      name: item.name,
      processed: true
    };
  });
}

// 如果可能,直接修改原对象
function processData(data) {
  data.forEach(item => {
    item.processed = true;
  });
  return data;
}
  1. 使用对象池
// 对象池模式,重用对象
class ObjectPool {
  constructor(createFn, resetFn) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
  }

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

  release(obj) {
    this.resetFn(obj);
    this.pool.push(obj);
  }
}

// 使用
const pool = new ObjectPool(
  () => ({ x: 0, y: 0 }),
  obj => { obj.x = 0; obj.y = 0; }
);

const obj = pool.acquire();
// 使用对象
pool.release(obj); // 归还到池中,而不是销毁
  1. 避免在循环中创建函数
// 不好的做法
for (let i = 0; i < 1000; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// 好的做法
function createHandler(i) {
  return function() {
    console.log(i);
  };
}

for (let i = 0; i < 1000; i++) {
  setTimeout(createHandler(i), 1000);
}
  1. 使用 WeakMap 和 WeakSet
// WeakMap 的键是弱引用,不会阻止垃圾回收
const weakMap = new WeakMap();
const obj = {};

weakMap.set(obj, 'data');
// 当 obj 没有其他引用时,weakMap 中的条目会被自动清除

内存监控

  1. 使用 Chrome DevTools
// 在控制台中使用
// performance.memory 可以查看内存使用情况
console.log(performance.memory);
// {
//   usedJSHeapSize: 10000000,
//   totalJSHeapSize: 20000000,
//   jsHeapSizeLimit: 2000000000
// }
  1. 使用 Performance API
// 记录内存使用
const memoryBefore = performance.memory.usedJSHeapSize;
// 执行操作
doSomething();
const memoryAfter = performance.memory.usedJSHeapSize;
console.log(`内存使用: ${(memoryAfter - memoryBefore) / 1024 / 1024} MB`);
  1. 手动触发垃圾回收(仅开发环境)
// Chrome DevTools 中可以使用
// 在控制台执行:window.gc()
// 需要启动 Chrome 时添加参数:--js-flags="--expose-gc"

常见问题

  1. 循环引用会被回收吗?
// 标记清除算法可以处理循环引用
let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };

obj1.ref = obj2;
obj2.ref = obj1;

obj1 = null;
obj2 = null;
// 两个对象都会被回收(标记清除算法)
  1. WeakMap 和 WeakSet 的作用
// WeakMap 的键必须是对象,且是弱引用
const weakMap = new WeakMap();
let obj = { id: 1 };

weakMap.set(obj, 'data');
obj = null; // obj 可以被回收,weakMap 中的条目也会被清除

// 普通 Map 会阻止垃圾回收
const map = new Map();
let obj2 = { id: 2 };
map.set(obj2, 'data');
obj2 = null; // obj2 不会被回收,因为 map 持有引用
  1. 闭包会导致内存泄漏吗?
// 不一定,取决于闭包引用的内容
function createClosure() {
  const largeData = new Array(1000000).fill(0);
  
  return function() {
    // 如果闭包引用了 largeData,largeData 不会被回收
    console.log(largeData.length);
  };
}

// 如果闭包没有引用外部变量,不会有问题
function createClosure2() {
  const largeData = new Array(1000000).fill(0);
  
  return function() {
    // 没有引用 largeData,largeData 可以被回收
    console.log('hello');
  };
}

最佳实践

  1. 及时清理资源
class ResourceManager {
  constructor() {
    this.timers = [];
    this.listeners = [];
  }

  addTimer(callback, delay) {
    const id = setInterval(callback, delay);
    this.timers.push(id);
    return id;
  }

  addListener(element, event, handler) {
    element.addEventListener(event, handler);
    this.listeners.push({ element, event, handler });
  }

  cleanup() {
    this.timers.forEach(id => clearInterval(id));
    this.listeners.forEach(({ element, event, handler }) => {
      element.removeEventListener(event, handler);
    });
    this.timers = [];
    this.listeners = [];
  }
}
  1. 避免在全局作用域存储大量数据
// 不好的做法
window.largeData = new Array(1000000).fill(0);

// 好的做法:使用模块作用域或函数作用域
(function() {
  const largeData = new Array(1000000).fill(0);
  // 使用 largeData
})();
  1. 使用事件委托而不是为每个元素添加监听器
// 不好的做法
document.querySelectorAll('button').forEach(button => {
  button.addEventListener('click', handler);
});

// 好的做法:事件委托
document.addEventListener('click', function(e) {
  if (e.target.tagName === 'BUTTON') {
    handler(e);
  }
});
  1. 使用 requestAnimationFrame 而不是 setInterval
// setInterval 可能在某些情况下导致内存问题
// requestAnimationFrame 更适合动画
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

总结

  • JavaScript 使用自动垃圾回收,主要使用标记清除算法
  • 注意避免内存泄漏:及时清理定时器、事件监听器、解除引用
  • 使用 WeakMap 和 WeakSet 可以避免阻止垃圾回收
  • 闭包要谨慎使用,避免持有不必要的引用
  • 使用工具监控内存使用情况,及时发现内存泄漏