Skip to content

JavaScript 内存管理

内存生命周期

JavaScript 的内存管理遵循以下生命周期:

1. 分配内存(Allocation)

2. 使用内存(Usage)

3. 释放内存(Release)

内存分配

1. 值类型的内存分配

javascript
// 基本类型存储在栈(Stack)中
let number = 42;           // 分配 8 字节(64位系统)
let string = "hello";      // 字符串本身在堆中,引用在栈中
let boolean = true;        // 分配 1 字节
let nullValue = null;      // 特殊值
let undefinedValue;        // 特殊值

2. 引用类型的内存分配

javascript
// 对象存储在堆(Heap)中
let obj = {
  name: "Alice",    // 分配字符串内存
  age: 25,          // 分配数字内存
  hobbies: []       // 分配数组内存
};

let arr = [1, 2, 3, 4, 5];  // 分配数组内存

let func = function() {      // 分配函数内存
  console.log("Hello");
};

3. 隐式内存分配

javascript
// 字符串拼接
let str = "Hello" + " " + "World";  // 创建新字符串

// 数组操作
let arr1 = [1, 2, 3];
let arr2 = arr1.map(x => x * 2);    // 创建新数组

// 对象扩展
let obj1 = { a: 1 };
let obj2 = { ...obj1, b: 2 };       // 创建新对象

// 闭包
function createCounter() {
  let count = 0;                     // 分配内存
  return function() {                // 闭包保持对 count 的引用
    return ++count;
  };
}

栈内存(Stack)与堆内存(Heap)

栈内存特点

javascript
// 栈内存:LIFO(后进先出)
function foo() {
  let a = 10;        // 压入栈
  let b = 20;        // 压入栈
  bar();
  // b 出栈
  // a 出栈
}

function bar() {
  let c = 30;        // 压入栈
  // c 出栈
}

特点

  • 快速分配和释放
  • 自动管理
  • 容量有限(栈溢出)
  • 存储基本类型和引用

堆内存特点

javascript
// 堆内存:动态分配
let obj1 = { value: 1 };  // 在堆中分配
let obj2 = { value: 2 };  // 在堆中分配
let obj3 = obj1;          // 复制引用,不分配新内存

obj1 = null;              // 不立即释放,等待 GC

特点

  • 动态大小
  • 手动管理(通过 GC)
  • 容量较大
  • 存储对象和数组

内存模型示例

javascript
function createPerson(name, age) {
  // 栈:name(引用)、age(值)
  // 堆:name 指向的字符串对象
  
  let person = {      // 栈:person(引用)
    name: name,       // 堆:person 对象
    age: age
  };
  
  return person;      // 返回引用
}

let alice = createPerson("Alice", 25);
// 栈:alice(引用)
// 堆:person 对象、"Alice" 字符串

let bob = alice;
// 栈:bob(引用,指向同一个对象)
// 堆:没有新分配

内存泄漏的原因和场景

1. 全局变量

javascript
// ❌ 意外的全局变量
function createLeak() {
  leak = "I'm a global variable";  // 没有 var/let/const
}

createLeak();
// window.leak 永远存在,无法被 GC

// ✅ 使用严格模式避免
'use strict';
function createNoLeak() {
  leak = "This will throw an error";  // ReferenceError
}

2. 被遗忘的定时器和回调

javascript
// ❌ 不良实践
let data = loadHugeData();

setInterval(() => {
  doSomethingWith(data);
}, 1000);

// data 永远不会被释放

// ✅ 良好实践
let data = loadHugeData();

const timer = setInterval(() => {
  if (shouldStop()) {
    clearInterval(timer);
    data = null;
  } else {
    doSomethingWith(data);
  }
}, 1000);

3. 闭包

javascript
// ❌ 闭包导致的内存泄漏
function createHandler() {
  let largeData = new Array(1000000).fill('*');
  
  // 即使不使用 largeData,它仍被闭包引用
  return {
    handle: function() {
      console.log('handling');
      // largeData 被保留在内存中
    }
  };
}

let handler = createHandler();

// ✅ 避免不必要的闭包引用
function createHandler() {
  let largeData = new Array(1000000).fill('*');
  let smallData = processData(largeData);
  largeData = null;  // 显式释放
  
  return {
    handle: function() {
      console.log(smallData);
    }
  };
}

4. DOM 引用

javascript
// ❌ DOM 引用泄漏
let elements = [];

function addElement() {
  let div = document.createElement('div');
  div.id = 'myDiv';
  document.body.appendChild(div);
  elements.push(div);  // 保存引用
}

function removeElement() {
  let div = document.getElementById('myDiv');
  document.body.removeChild(div);
  // elements 数组仍然引用已移除的 DOM 节点
}

// ✅ 正确清理
function removeElement() {
  let div = document.getElementById('myDiv');
  document.body.removeChild(div);
  elements = elements.filter(el => el !== div);  // 移除引用
}

5. 事件监听器

javascript
// ❌ 事件监听器泄漏
class Component {
  constructor() {
    this.data = new Array(1000000);
    this.handleClick = () => {
      console.log(this.data.length);
    };
  }
  
  mount() {
    document.addEventListener('click', this.handleClick);
  }
  
  // 忘记移除监听器
}

// ✅ 正确清理
class Component {
  constructor() {
    this.data = new Array(1000000);
    this.handleClick = () => {
      console.log(this.data.length);
    };
  }
  
  mount() {
    document.addEventListener('click', this.handleClick);
  }
  
  unmount() {
    document.removeEventListener('click', this.handleClick);
    this.data = null;
  }
}

6. 缓存

javascript
// ❌ 无限增长的缓存
const cache = {};

function getUser(id) {
  if (!cache[id]) {
    cache[id] = fetchUser(id);
  }
  return cache[id];
}

// cache 会无限增长

// ✅ 使用 LRU 缓存或 WeakMap
class LRUCache {
  constructor(maxSize) {
    this.maxSize = maxSize;
    this.cache = new Map();
  }
  
  get(key) {
    if (!this.cache.has(key)) return null;
    
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);  // 移到最后(最近使用)
    return value;
  }
  
  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    this.cache.set(key, value);
    
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);  // 删除最久未使用的
    }
  }
}

// 或使用 WeakMap(键必须是对象)
const weakCache = new WeakMap();
function cacheObject(obj, value) {
  weakCache.set(obj, value);
  // 当 obj 不再被引用时,自动清理
}

内存优化技巧

1. 对象池(Object Pooling)

javascript
class Vector2D {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
  
  set(x, y) {
    this.x = x;
    this.y = y;
    return this;
  }
}

class Vector2DPool {
  constructor(initialSize = 10) {
    this.pool = [];
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(new Vector2D());
    }
  }
  
  acquire(x, y) {
    if (this.pool.length > 0) {
      return this.pool.pop().set(x, y);
    }
    return new Vector2D(x, y);
  }
  
  release(vector) {
    this.pool.push(vector);
  }
}

// 使用示例
const pool = new Vector2DPool(100);

function gameLoop() {
  const v1 = pool.acquire(10, 20);
  const v2 = pool.acquire(30, 40);
  
  // 使用 v1 和 v2
  
  pool.release(v1);
  pool.release(v2);
}

2. 及时释放引用

javascript
// ✅ 处理完大对象后立即释放
async function processLargeData() {
  let data = await loadLargeData();  // 10MB
  
  let result = await process(data);
  
  data = null;  // 显式释放,不等待函数结束
  
  return result;
}

3. 使用 WeakMap 和 WeakSet

javascript
// 传统 Map 会阻止 GC
const userCache = new Map();
let user = { id: 1, name: 'Alice' };
userCache.set(user, { lastSeen: Date.now() });
user = null;  // userCache 仍然持有对原对象的强引用

// WeakMap 允许 GC
const weakUserCache = new WeakMap();
let user2 = { id: 2, name: 'Bob' };
weakUserCache.set(user2, { lastSeen: Date.now() });
user2 = null;  // 对象可以被 GC,缓存项自动清理

// WeakSet 示例
const visitedObjects = new WeakSet();

function markVisited(obj) {
  visitedObjects.add(obj);
}

function hasVisited(obj) {
  return visitedObjects.has(obj);
}

4. 分批处理大数据

javascript
// ❌ 一次性处理所有数据
function processAll(data) {
  return data.map(item => expensiveOperation(item));
}

// ✅ 分批处理
async function processBatch(data, batchSize = 100) {
  const results = [];
  
  for (let i = 0; i < data.length; i += batchSize) {
    const batch = data.slice(i, i + batchSize);
    const batchResults = batch.map(item => expensiveOperation(item));
    results.push(...batchResults);
    
    // 让出主线程,允许 GC 运行
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  
  return results;
}

5. 避免内存抖动

javascript
// ❌ 频繁创建临时对象
function animate() {
  for (let i = 0; i < 1000; i++) {
    const temp = { x: i, y: i * 2 };  // 每帧创建 1000 个对象
    draw(temp);
  }
  requestAnimationFrame(animate);
}

// ✅ 复用对象
const tempVector = { x: 0, y: 0 };

function animate() {
  for (let i = 0; i < 1000; i++) {
    tempVector.x = i;
    tempVector.y = i * 2;
    draw(tempVector);
  }
  requestAnimationFrame(animate);
}

6. 字符串优化

javascript
// ❌ 字符串拼接创建多个临时字符串
function buildString(items) {
  let result = '';
  for (let item of items) {
    result += item + ',';  // 每次拼接创建新字符串
  }
  return result;
}

// ✅ 使用数组 join
function buildString(items) {
  return items.join(',');
}

// 或使用模板字符串
function buildString(items) {
  return items.map(item => `${item}`).join(',');
}

内存监控和调试

1. 浏览器工具

Chrome DevTools Memory Profiler

javascript
// 记录堆快照的时机
console.log('Before operation');
// 操作 1:拍摄快照 1

let data = new Array(1000000);
console.log('After allocation');
// 操作 2:拍摄快照 2

data = null;
console.log('After cleanup');
// 操作 3:拍摄快照 3

// 比较快照 2 和快照 3,查看是否成功释放

Performance Monitor

javascript
// 实时监控内存使用
// 1. 打开 DevTools
// 2. Cmd/Ctrl + Shift + P
// 3. 输入 "Show Performance Monitor"
// 4. 观察 JS Heap Size 和 DOM Nodes

2. Node.js 内存监控

javascript
// 获取内存使用情况
function printMemoryUsage() {
  const usage = process.memoryUsage();
  console.log({
    rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,           // 常驻集大小
    heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`, // 堆总大小
    heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`,   // 已使用堆
    external: `${(usage.external / 1024 / 1024).toFixed(2)} MB`    // C++ 对象
  });
}

// 监控内存增长
setInterval(() => {
  printMemoryUsage();
  if (global.gc) {
    global.gc();  // 手动触发 GC(需要 --expose-gc)
    console.log('After GC:');
    printMemoryUsage();
  }
}, 5000);

3. 自定义内存监控

javascript
class MemoryMonitor {
  constructor() {
    this.measurements = [];
  }
  
  measure(label) {
    if (performance.memory) {
      this.measurements.push({
        label,
        timestamp: Date.now(),
        usedJSHeapSize: performance.memory.usedJSHeapSize,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
      });
    }
  }
  
  report() {
    console.table(this.measurements);
  }
  
  checkLeak(threshold = 0.9) {
    if (performance.memory) {
      const ratio = performance.memory.usedJSHeapSize / 
                    performance.memory.jsHeapSizeLimit;
      if (ratio > threshold) {
        console.warn(`Memory usage at ${(ratio * 100).toFixed(2)}%`);
        return true;
      }
    }
    return false;
  }
}

// 使用示例
const monitor = new MemoryMonitor();

monitor.measure('Start');
// 执行一些操作
let data = new Array(1000000);
monitor.measure('After allocation');

data = null;
monitor.measure('After cleanup');

monitor.report();

内存限制

V8 内存限制

javascript
// 默认限制(64位系统)
// - 老生代:约 1.4 GB
// - 新生代:约 32 MB

// Node.js 启动时设置内存限制
// node --max-old-space-size=4096 app.js  // 4GB
// node --max-new-space-size=16 app.js    // 16MB

检测内存限制

javascript
// 获取 V8 堆统计信息(Node.js)
const v8 = require('v8');
const heapStats = v8.getHeapStatistics();

console.log({
  totalHeapSize: `${(heapStats.total_heap_size / 1024 / 1024).toFixed(2)} MB`,
  usedHeapSize: `${(heapStats.used_heap_size / 1024 / 1024).toFixed(2)} MB`,
  heapSizeLimit: `${(heapStats.heap_size_limit / 1024 / 1024).toFixed(2)} MB`
});

最佳实践总结

内存管理原则

  1. 及时释放不再使用的引用
  2. 使用合适的数据结构(Map vs Object,WeakMap vs Map)
  3. 避免意外的全局变量
  4. 清理定时器和事件监听器
  5. 注意闭包的使用
  6. 合理使用缓存(设置大小限制)
  7. 分批处理大数据
  8. 复用对象减少 GC 压力

代码检查清单

  • [ ] 所有定时器都有对应的清理逻辑
  • [ ] 所有事件监听器都在组件销毁时移除
  • [ ] 大对象使用完后及时置为 null
  • [ ] 避免在循环中创建函数或对象
  • [ ] 缓存有大小限制或使用 WeakMap
  • [ ] DOM 引用在元素移除后被清理
  • [ ] 闭包中不保留不必要的外层变量

参考资源

MIT Licensed | Keep Learning.