内存管理
JavaScript 使用垃圾回收机制自动管理内存,但开发者仍需注意内存泄漏问题。理解内存管理原理有助于编写高性能代码。
一、JavaScript 内存模型
1. 内存结构
┌─────────────────────────────────────────────────────┐
│ V8 内存结构 │
├─────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ Stack 栈内存 │ │ Heap 堆内存 │ │
│ │ │ │ ┌───────────────────┐ │ │
│ │ - 原始类型 │ │ │ New Space │ │ │
│ │ - 引用地址 │ │ │ (新生代 1-8MB) │ │ │
│ │ - 执行上下文 │ │ └───────────────────┘ │ │
│ │ │ │ ┌───────────────────┐ │ │
│ └─────────────────┘ │ │ Old Space │ │ │
│ │ │ (老生代) │ │ │
│ │ └───────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────┘2. 栈内存与堆内存
| 类型 | 栈内存 (Stack) | 堆内存 (Heap) |
|---|---|---|
| 存储内容 | 原始类型、引用地址 | 对象、数组、函数 |
| 大小限制 | 固定大小 | 动态分配 |
| 分配速度 | 快 | 较慢 |
| 生命周期 | 函数调用结束释放 | 垃圾回收释放 |
| 线程共享 | 线程独立 | 线程共享 |
// 栈内存 - 原始类型
let num = 123; // 直接存储值
let str = 'hello'; // 直接存储值
let bool = true; // 直接存储值
// 堆内存 - 引用类型
const obj = { a: 1 }; // 栈存引用,堆存对象
const arr = [1, 2]; // 栈存引用,堆存数组3. 新生代与老生代
对象生命周期:
新生代 (New Space)
┌─────────────────┐
│ 新创建的对象 │ ← 短生命周期对象
│ Scavenge 算法 │
└────────┬────────┘
│ 存活足够久
▼
老生代 (Old Space)
┌─────────────────┐
│ 长生命周期对象 │ ← 标记清除算法
│ Mark-Sweep │
└─────────────────┘二、垃圾回收机制
1. 标记清除算法
标记清除过程:
Step 1: 标记阶段
┌─────────────────────────────────┐
│ Root → A → B → C │
│ ↓ │
│ D ← E (不可达) │
│ │
│ ✓ A, B, C, D: 可达(标记) │
│ ✗ E: 不可达(未标记) │
└─────────────────────────────────┘
Step 2: 清除阶段
┌─────────────────────────────────┐
│ Root → A → B → C │
│ ↓ │
│ D [回收 E] │
│ │
│ 清除未标记的对象 │
└─────────────────────────────────┘// 垃圾回收示例
function createObjects() {
const obj1 = { name: 'obj1' }; // 创建对象
const obj2 = { name: 'obj2' }; // 创建对象
return obj1; // obj1 可达,obj2 在函数结束后不可达
}
const result = createObjects();
// obj2 被垃圾回收2. 引用计数算法
// 引用计数的问题:循环引用
// ❌ 循环引用导致内存泄漏(旧版 IE)
function createCircular() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2; // obj2 引用计数 +1
obj2.ref = obj1; // obj1 引用计数 +1
// 函数结束后,obj1 和 obj2 的引用计数都为 1
// 无法被回收
}
// ✅ 现代浏览器使用标记清除,可处理循环引用
// 但仍需注意闭包等特殊情况3. 分代回收
// V8 分代回收策略
// 新生代:Scavenge 算法(复制算法)
// - 将内存分为两半:From 空间和 To 空间
// - 存活对象从 From 复制到 To
// - 清空 From,交换 From 和 To
// 老生代:标记清除 + 标记整理
// - 标记清除:标记存活对象,清除未标记对象
// - 标记整理:移动存活对象,消除内存碎片4. 垃圾回收触发条件
| 条件 | 说明 |
|---|---|
| 内存不足 | 堆内存接近上限 |
| 定期回收 | 空闲时自动执行 |
| 手动触发 | global.gc()(需启用) |
三、常见内存泄漏
1. 全局变量
// ❌ 意外的全局变量
function leak() {
bar = 'This is a global variable'; // 未声明,成为全局变量
}
// ✅ 使用严格模式
'use strict';
function noLeak() {
const bar = 'local variable';
}
// ✅ 使用模块作用域
const moduleScope = (() => {
let privateVar = 'private';
return {
getVar: () => privateVar
};
})();2. 闭包
// ❌ 闭包持有大对象
function createClosure() {
const largeData = new Array(1000000).fill('x');
return function() {
console.log(largeData.length); // largeData 无法被回收
};
}
// ✅ 释放不需要的数据
function createClosure() {
const largeData = new Array(1000000).fill('x');
const length = largeData.length; // 只保留需要的值
return function() {
console.log(length);
};
// largeData 在函数结束后可被回收
}
// ✅ 使用 let 和块级作用域
function createClosures() {
const closures = [];
for (let i = 0; i < 1000; i++) { // 使用 let
closures.push(() => i); // 每个闭包捕获独立的 i
}
return closures;
}3. 事件监听
// ❌ 未清理事件监听
class Component {
mount() {
window.addEventListener('resize', this.handleResize);
}
}
// ✅ 组件销毁时清理
class Component {
constructor() {
this.handleResize = this.handleResize.bind(this);
}
// 在挂载时监听
mount() {
this.controller = new AbortController();
window.addEventListener('resize', this.handleResize, {
signal: this.controller.signal,
});
}
unmount() {
this.controller.abort(); // 自动移除所有监听器
}
}4. 定时器
// ❌ 未清理定时器
function startTimer() {
setInterval(() => {
// 持续运行,即使组件已销毁
}, 1000);
}
// ✅ 清理定时器
class Timer {
start() {
this.timerId = setInterval(() => {}, 1000);
}
stop() {
clearInterval(this.timerId);
}
}
// ✅ 返回清理函数
function startTimer(callback, delay) {
const timerId = setInterval(callback, delay);
return () => clearInterval(timerId);
}
// 使用
const cleanup = startTimer(() => {}, 1000);
cleanup(); // 停止定时器5. DOM 引用
// ❌ 保留已删除 DOM 的引用
const elements = [];
function addElement() {
const el = document.createElement('div');
document.body.appendChild(el);
elements.push(el); // 保存引用
}
function removeElement(el) {
document.body.removeChild(el);
// elements 数组仍持有引用,导致内存泄漏
}
// ✅ 移除 DOM 时同时清理引用
function removeElementSafe(el) {
const index = elements.indexOf(el);
if (index > -1) {
elements.splice(index, 1);
}
document.body.removeChild(el);
}6. console.log
// ❌ console.log 持有对象引用
function debug() {
const largeObject = { /* 大量数据 */ };
console.log('Debug:', largeObject);
// 在开发者工具中,largeObject 不会被回收
}
// ✅ 生产环境移除 console
if (process.env.NODE_ENV === 'production') {
console.log = () => {};
}
// ✅ 使用条件日志
function debug(message, data) {
if (DEBUG_MODE) {
console.log(message, JSON.parse(JSON.stringify(data)));
}
}四、内存优化技巧
1. 使用 WeakMap/WeakSet
// WeakMap 的键是弱引用
const cache = new WeakMap();
function cacheData(obj, data) {
cache.set(obj, data);
}
// 当 obj 被回收时,cache 中的条目自动清除
// 应用示例:存储 DOM 元素的额外数据
const elementData = new WeakMap();
function bindData(element, data) {
elementData.set(element, data);
}
function getData(element) {
return elementData.get(element);
}
// 元素移除后,数据自动释放2. 及时释放引用
// ✅ 手动释放大对象
function process() {
let largeData = new Array(1000000).fill(0);
// 使用 largeData
const result = largeData.reduce((a, b) => a + b, 0);
// 释放引用
largeData = null;
return result;
}
// ✅ 清理对象属性
function clearObject(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
}
// ✅ 使用 Object.create(null) 创建纯净对象
const cleanObj = Object.create(null);
// 没有原型链,更节省内存3. 对象池模式
class ObjectPool {
constructor(factory, initialSize = 10) {
this.factory = factory;
this.pool = [];
// 预创建对象
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.factory());
}
}
acquire() {
// 有可用对象则取出,否则新建
return this.pool.pop() || this.factory();
}
release(obj) {
this.pool.push(obj);
}
}
// 使用对象池
const particlePool = new ObjectPool(() => ({ x: 0, y: 0, vx: 0, vy: 0 }));
function createParticle() {
const particle = particlePool.acquire();
return particle;
}
function destroyParticle(particle) {
particlePool.release(particle);
}4. 避免创建临时对象
// ❌ 循环中创建新对象
function processItems(items) {
return items.map(item => {
const result = { value: item.value }; // 每次创建新对象
return result;
});
}
// ✅ 重用对象
function processItems(items) {
const result = [];
const temp = { value: null };
for (const item of items) {
temp.value = item.value;
result.push({ ...temp });
}
return result;
}
// ✅ 使用 TypedArray 处理数值数据
const floats = new Float64Array(1000);
for (let i = 0; i < 1000; i++) {
floats[i] = Math.random();
}5. 字符串优化
// ❌ 大量字符串拼接
let html = '';
for (let i = 0; i < 10000; i++) {
html += '<div>' + i + '</div>'; // 每次创建新字符串
}
// ✅ 使用数组 join
const parts = [];
for (let i = 0; i < 10000; i++) {
parts.push('<div>' + i + '</div>');
}
const html = parts.join('');
// ✅ 使用模板字符串
const items = [1, 2, 3, 4, 5];
const html = items.map(i => `<div>${i}</div>`).join('');五、内存监控与分析
1. Chrome DevTools
Memory 面板功能:
1. Heap Snapshot(堆快照)
- 查看当前内存中的所有对象
- 对比不同时间点的快照,找出泄漏
2. Allocation instrumentation on timeline(分配时间线)
- 记录内存分配过程
- 蓝色柱子表示内存分配
3. Allocation sampling(分配采样)
- 低开销的内存分析
- 查看函数的内存分配情况2. Performance API
// 获取内存使用情况
function getMemoryInfo() {
if (performance.memory) {
return {
usedJSHeapSize: performance.memory.usedJSHeapSize, // 已用堆内存
totalJSHeapSize: performance.memory.totalJSHeapSize, // 总堆内存
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, // 堆内存限制
// 计算使用率
usagePercent: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit * 100).toFixed(2)
};
}
return null;
}
console.log(getMemoryInfo());3. 内存监控
class MemoryMonitor {
constructor(options = {}) {
this.interval = options.interval || 5000;
this.warningThreshold = options.warningThreshold || 0.8;
this.history = [];
this.timer = null;
}
start() {
this.timer = setInterval(() => {
const memory = this.getMemory();
this.history.push({
time: Date.now(),
...memory,
});
// 内存使用超过警告阈值时报警
if (memory.usageRatio > this.warningThreshold) {
console.warn(
`内存占用过高: ${(memory.usageRatio * 100).toFixed(1)}%`
);
}
}, this.interval);
}
stop() {
clearInterval(this.timer);
}
getMemory() {
const memory = performance.memory || {};
const { usedJSHeapSize = 0, jsHeapSizeLimit = 1 } = memory;
return {
used: usedJSHeapSize,
usageRatio: usedJSHeapSize / jsHeapSizeLimit,
};
}
}
// 使用
const monitor = new MemoryMonitor({ interval: 10000 });
monitor.start();4. 检测内存泄漏
// 堆快照对比法
async function detectMemoryLeak() {
// 1. 记录初始状态
const snapshot1 = await takeHeapSnapshot();
// 2. 执行可能泄漏的操作
for (let i = 0; i < 100; i++) {
performOperation();
}
// 3. 强制垃圾回收(需要启动时添加 --expose-gc)
if (global.gc) {
global.gc();
const snapshot2 = await takeHeapSnapshot();
// 4. 对比堆快照,计算内存变化
const leaked = snapshot2.totalSize - snapshot1.totalSize;
console.log('内存变化:', (leaked / 1024 / 1024).toFixed(2), 'MB');
if (leaked > 1024 * 1024) { // 超过 1MB
console.warn('可能存在内存泄漏');
}
}
}六、最佳实践
1. 内存管理检查清单
- 使用严格模式避免全局变量
- 及时清理事件监听器
- 及时清理定时器
- 避免闭包捕获不必要的数据
- 使用 WeakMap 存储对象元数据
- 大对象使用后置为 null
- 生产环境移除调试日志
- 使用对象池重用对象
- 使用 WeakRef 进行条件性缓存
- 使用 FinalizationRegistry 监控对象回收
2. 代码模式对比
| 模式 | 内存影响 | 推荐程度 |
|---|---|---|
| 模块作用域 | 隔离全局变量 | ⭐⭐⭐ |
| WeakMap | 弱引用存储 | ⭐⭐⭐ |
| WeakRef | 条件性缓存 | ⭐⭐ |
| 对象池 | 重用对象 | ⭐⭐ |
| 块级作用域 | 及时释放 | ⭐⭐⭐ |
| 闭包缓存 | 谨慎使用 | ⭐⭐ |
3. WeakRef 与 FinalizationRegistry(ES2021)
WeakRef 和 FinalizationRegistry 是 ES2021 引入的 API,用于更精细的内存管理:
// WeakRef - 创建对对象的弱引用
let target = { data: new Array(1000000).fill("x") }
const ref = new WeakRef(target)
// 获取引用对象(可能已被回收)
const obj = ref.deref()
if (obj) {
console.log("对象仍然存在:", obj.data.length)
} else {
console.log("对象已被垃圾回收")
}
// FinalizationRegistry:对象被回收时执行清理回调
const registry = new FinalizationRegistry((heldValue) => {
console.log(`资源被回收: ${heldValue}`)
})
let resource = { id: "resource-1", data: new ArrayBuffer(1024 * 1024) }
registry.register(resource, "resource-1")
// 当 resource 被垃圾回收时,回调会被调用
resource = null // 允许垃圾回收⚠️ WeakRef 使用注意事项: -
WeakRef适用于缓存等可接受对象随时消失的场景
- 避免在核心逻辑中依赖
WeakRef.deref()的结果FinalizationRegistry的回调时机不可预测,不能用于关键资源释放- 对于确定性的资源释放,请使用 ES2025 的
using声明
七、常见问题解答
Q1: 如何判断是否存在内存泄漏?
A: 判断方法:
// 1. Chrome DevTools Memory 面板
// - 多次操作后查看堆大小是否持续增长
// 2. Performance Monitor
// - 打开 DevTools → More tools → Performance Monitor
// - 观察 JS heap size 曲线
// 3. 代码检测
const monitor = new MemoryMonitor();
monitor.start();
// 执行操作
performAction();
// 查看报告
console.log(monitor.getReport());Q2: WeakMap 和 Map 有什么区别?
A: 主要区别:
// Map - 强引用
const map = new Map();
let obj = { name: 'test' };
map.set(obj, 'data');
obj = null; // obj 不会被回收,map 仍持有引用
// WeakMap - 弱引用
const weakMap = new WeakMap();
obj = { name: 'test' };
weakMap.set(obj, 'data');
obj = null; // obj 可以被回收,WeakMap 条目自动清除
// WeakMap 特点:
// 1. 键必须是对象
// 2. 键是弱引用,不阻止垃圾回收
// 3. 不可遍历
// 4. 没有 size 属性Q3: 如何处理大型数据集?
A: 处理策略:
// 1. 分页加载
async function loadPage(page, size) {
const data = await fetch(`/api/data?page=${page}&size=${size}`);
return data;
}
// 2. 流式处理
async function processStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 处理数据块
processChunk(value);
}
}
// 3. 虚拟滚动
// 只渲染可见区域的数据
// 4. Web Worker
// 将数据处理放到 Worker 中
const worker = new Worker('data-processor.js');
worker.postMessage(largeData);Q4: 何时需要手动触发垃圾回收?
A: 一般不需要,但在以下场景可以考虑:
// 仅在 Node.js 环境可用
if (global.gc) {
// 场景1:大量内存释放后
largeArray = null;
global.gc();
// 场景2:性能测试前
global.gc();
const start = performance.now();
// 测试代码
}Q5: 如何优化 SPA 应用的内存?
A: SPA 内存优化策略:
// 1. 路由切换时清理
router.beforeEach((to, from, next) => {
// 清理上一页的数据
store.dispatch('clearPageData', from.path);
next();
});
// 2. 组件销毁时清理
onUnmounted(() => {
// 取消请求
controller.abort();
// 清理事件
eventBus.off('event', handler);
// 清理定时器
clearInterval(timer);
});
// 3. 缓存策略
const cache = new LRUCache({
max: 100,
maxAge: 1000 * 60 * 5 // 5 分钟
});
// 4. 懒加载
const LazyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);八、V8 垃圾回收算法
V8 引擎采用分代回收策略,针对不同生命周期的对象使用不同的回收算法,以在回收效率和性能开销之间取得平衡。
1. 新生代:Scavenge 算法
新生代(New Space)通常容量为 1-8MB,用于存放新创建的短生命周期对象。Scavenge 算法基于 Cheney 的半空间复制算法,将新生代堆内存等分为两个半空间:From 空间(活动空间)和 To 空间(空闲空间)。
Scavenge 回收过程:
回收前:
┌─────────────────────────┐
│ From 空间(活动) │ To 空间(空闲)
│ ┌───┐ ┌───┐ ┌───┐ │ ┌─────────────────┐
│ │ A │ │ B │ │ C │ │ │ │
│ │✓活│ │✗死│ │✓活│ │ │ (空) │
│ └───┘ └───┘ └───┘ │ │ │
└─────────────────────────┘ └─────────────────┘
回收后:
┌─────────────────────────┐
│ From 空间(清空) │ To 空间(活动)
│ ┌─────────────────┐ │ ┌───┐ ┌───┐
│ │ │ │ │ A │ │ C │ ← 存活对象复制过来
│ │ (空) │ │ │✓活│ │✓活│ 且排列紧凑
│ │ │ │ └───┘ └───┘
└─────────────────────────┘ └─────────────────┘
最后交换 From/To 角色,为下次回收做准备// Scavenge 算法特点:
// 1. 只复制存活对象,死对象直接被遗弃
// 2. 复制后对象排列紧凑,无内存碎片
// 3. 时间复杂度与存活对象数量成正比
// 4. 牺牲一半空间换取效率,适合存活率低的新生代
// 对象晋升条件:
// -经历过一次 Scavenge 回收仍存活 → 晋升到老生代
// -To 空间使用率超过 25% → 直接晋升到老生代适用场景:新生代对象通常"朝生夕死",存活率极低,Scavenge 只需复制少量存活对象,效率很高。
2. 老生代:Mark-Sweep & Mark-Compact
老生代(Old Space)存放生命周期较长的对象,容量远大于新生代。V8 对老生代采用**标记清除(Mark-Sweep)与标记整理(Mark-Compact)**相结合的策略。
Mark-Sweep(标记清除):
标记阶段:从 Root 出发,递归标记所有可达对象
┌─────────────────────────────────┐
│ Root → A → B │
│ ↓ │
│ C D (不可达) │
│ │
│ ✓ A, B, C: 已标记(存活) │
│ ✗ D: 未标记(死亡) │
└─────────────────────────────────┘
整理阶段:将所有存活对象移动到连续内存,消除碎片
┌─────────────────────────────────┐
│ 整理前: │
│ Root → A → B │
│ ┌─────┬─────┬─────┬──────────┐ │
│ │ A │ D │ B │ 空闲 │ │
│ └─────┴─────┴─────┴──────────┘ │
│ │
│ 整理后: │
│ Root → A → B │
│ ┌───┬───┬───┬───────────────┐ │
│ │ A │ B │ C │ 连续空闲 │ │
│ └───┴───┴───┴───────────────┘ │
│ ✓ 无碎片,但移动对象有开销 │
└─────────────────────────────────┘// Mark-Sweep 与 Mark-Compact 的协作策略:
// 1. 优先使用 Mark-Sweep(速度快,不移动对象)
// 2. 当内存碎片过多时,使用 Mark-Compact(整理碎片)
// 3. Mark-Compact 需要移动对象并更新引用,开销较大
// 老生代回收流程:
// 标记 → 清除 → (碎片过多时) 整理为什么不全用 Mark-Compact? 因为整理需要移动所有存活对象并更新引用指针,开销远大于简单的清除。只有在碎片化严重影响分配时才触发整理。
3. 增量标记(Incremental Marking)
全量标记会导致较长的 GC 停顿(Stop-The-World),影响应用响应性。V8 引入增量标记,将标记任务拆分为多个小步骤,穿插在 JavaScript 执行之间。
全量标记(长停顿):
JS执行 ████████ GC停顿 ████████████ JS执行 ████████
↑ 长时间阻塞
增量标记(短停顿):
JS ██ 标记 ██ JS ██ 标记 ██ JS ██ 标记 ██ JS ██ 清除 ██ JS ██
↑ ↑ ↑ ↑ ↑ 短暂停顿
每次只做一小部分标记工作,避免长时间阻塞主线程// 增量标记的三阶段:
// 1. 标记根节点 → 速度快,停顿极短
// 2. 标记引用链 → 拆分为多个小步骤
// 3. 标记完成 → 执行清除
// 写屏障(Write Barrier):
// 增量标记期间,如果 JS 代码修改了已标记对象的引用
// 需要通过写屏障将修改的对象重新加入标记队列
const obj = { ref: oldTarget };
obj.ref = newTarget; // 触发写屏障,确保 newTarget 被正确标记核心权衡:增量标记用更多的总 GC 时间换取更短的单次停顿,提升用户感知的响应速度。
4. 并发回收(Concurrent GC)
V8 进一步引入并发回收机制,让 GC 的部分工作在辅助线程中执行,主线程继续运行 JavaScript,实现真正的无停顿回收。
并发回收示意:
主线程: JS执行 ████████████████████████████████████
辅助线程1: ████标记████████标记████
辅助线程2: ████标记████████清除████
辅助线程3: ████整理████
主线程几乎不暂停,GC 工作由辅助线程并行完成// V8 并发回收策略:
// 1. 并发标记(Concurrent Marking)
// - 辅助线程执行标记工作,主线程不暂停
// - 遇到写屏障同步问题,通过原子操作和写屏障协调
// 2. 并发清除(Concurrent Sweeping)
// - 辅助线程执行清除工作
// - 主线程可正常分配内存
// 3. 并发整理(Concurrent Compaction)
// - 辅助线程执行内存整理
// - 主线程通过页级锁定协调访问
// V8 的完整 GC 策略组合:
// ┌────────────┬──────────┬───────────┬───────────┐
// │ │ 新生代 │ 老生代 │ 全堆 │
// ├────────────┼──────────┼───────────┼───────────┤
// │ 主 GC │ Scavenge │ Mark-Sweep│ - │
// │ 增量 │ - │ Incremental│ - │
// │ 并发 │ - │ Concurrent│ Concurrent│
// │ 全堆 │ - │ - │ Mark-Compact│
// └────────────┴──────────┴───────────┴───────────┘并发回收的挑战:主线程和 GC 线程可能同时访问同一块内存,需要通过锁、原子操作和写屏障来保证数据一致性。V8 花费了大量工程努力来降低同步开销。
九、内存优化最佳实践
1. 避免创建不必要的闭包和大对象
// ❌ 闭包不必要地持有大对象
function setupHandler(data) {
const hugeData = new Array(1000000).fill('x');
element.addEventListener('click', () => {
console.log(hugeData.length); // 闭包持有 hugeData 引用
});
}
// ✅ 只提取需要的值
function setupHandler(data) {
const length = data.length;
element.addEventListener('click', () => {
console.log(length); // 闭包只持有基本类型值
});
}
// ❌ 循环中创建大量临时对象
function processItems(items) {
return items.map(item => ({ value: item, timestamp: Date.now() }));
}
// ✅ 复用对象或使用原始类型
function processItems(items) {
const now = Date.now();
return items.map(item => [item, now]); // 数组比对象更轻量
}2. 及时解除引用
// ❌ 大对象使用后仍被引用
class DataProcessor {
process() {
this.rawData = fetchLargeData(); // 一直挂在 this 上
const result = analyze(this.rawData);
return result;
// this.rawData 永远不会被回收
}
}
// ✅ 使用后置为 null
class DataProcessor {
process() {
let rawData = fetchLargeData();
const result = analyze(rawData);
rawData = null; // 允许 GC 回收
return result;
}
}
// ✅ 使用块级作用域自动释放
function process() {
{
const rawData = fetchLargeData(); // 块级作用域
const result = analyze(rawData);
// rawData 在块结束后即可被回收
}
// 此处 rawData 已不可达
return result;
}3. 谨慎使用全局变量
// ❌ 全局变量永远不会被回收
window.cachedData = loadLargeDataset();
// ❌ 遗漏声明的变量成为全局变量
function process() {
result = heavyComputation(); // 隐式全局变量
}
// ✅ 使用模块作用域
const cachedData = loadLargeDataset(); // 模块作用域,可被回收
// ✅ 严格模式防止隐式全局变量
'use strict';
function process() {
const result = heavyComputation(); // 必须声明
}
// ✅ 需要全局缓存时使用 WeakRef
const globalCache = new Map();
globalCache.set(key, new WeakRef(largeObject));
// largeObject 在没有其他引用时可被回收4. WeakRef 与 FinalizationRegistry 实战
// 场景:图片缓存,内存不足时允许 GC 回收
class ImageCache {
#cache = new Map();
#registry = new FinalizationRegistry(key => {
// 对象被回收后清理缓存条目
this.#cache.delete(key);
});
set(key, image) {
const ref = new WeakRef(image);
this.#cache.set(key, ref);
this.#registry.register(image, key);
}
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
return ref.deref(); // 可能返回 undefined(已被 GC)
}
}
// 使用:缓存大图
const cache = new ImageCache();
const largeImage = loadImage('hero-banner');
cache.set('hero-banner', largeImage);
// 后续获取时,如果内存紧张,largeImage 可能已被 GC 回收
const img = cache.get('hero-banner');
if (!img) {
// 重新加载
reloadImage('hero-banner');
}⚠️ ****WeakRef 和 FinalizationRegistry 不是内存管理的"银弹": -
WeakRef.deref()的结果随时可能变为undefined,不能依赖其确定性
FinalizationRegistry的回调时机不可控,可能延迟很久才触发- 它们适用于可接受数据丢失的缓存场景,不适用于核心业务逻辑
5. 使用 Chrome DevTools Memory 面板排查内存泄漏
排查流程:
1. 打开 DevTools → Memory 面板
2. 拍摄堆快照(Heap Snapshot)
┌─────────────────────────────────────┐
│ Snapshot 1 → 操作前基准 │
│ Snapshot 2 → 执行可疑操作后 │
│ Snapshot 3 → 再次执行可疑操作后 │
└─────────────────────────────────────┘
3. 对比快照:选择 Snapshot 2,筛选 "Objects allocated between Snapshot 1 and 2"
- 关注持续增长的对象类型(如 Detached DOM nodes、闭包等)
4. 查看保留路径(Retainers)
- 找到阻止 GC 回收的引用链
- 确认是否需要手动解除引用
5. 使用 Allocation Timeline 记录分配过程
- 蓝色柱子表示分配但未回收的内存
- 持续增长的蓝色区域 = 潜在泄漏// 常见泄漏模式在 DevTools 中的表现:
// 1. Detached DOM nodes
// 搜索 "Detached",找到脱离 DOM 树但仍被引用的元素
const detached = document.createElement('div');
document.body.appendChild(detached);
document.body.removeChild(detached);
// 如果仍有 JS 变量引用 detached,它不会被回收
// 2. 闭包泄漏
// 在 Retainers 中查看闭包捕获的变量
function createLeak() {
const leaked = new ArrayBuffer(1024 * 1024);
return () => leaked; // 闭包持有 leaked 引用
}
// 3. 事件监听器泄漏
// 检查 Event Listeners 面板
// 确认组件销毁时是否移除了所有监听器十、参考资料
提示:内存泄漏通常不易察觉,需要在开发阶段养成良好的编码习惯。定期使用 DevTools 检查内存使用情况。