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 Nodes2. 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`
});最佳实践总结
内存管理原则
- 及时释放不再使用的引用
- 使用合适的数据结构(Map vs Object,WeakMap vs Map)
- 避免意外的全局变量
- 清理定时器和事件监听器
- 注意闭包的使用
- 合理使用缓存(设置大小限制)
- 分批处理大数据
- 复用对象减少 GC 压力
代码检查清单
- [ ] 所有定时器都有对应的清理逻辑
- [ ] 所有事件监听器都在组件销毁时移除
- [ ] 大对象使用完后及时置为 null
- [ ] 避免在循环中创建函数或对象
- [ ] 缓存有大小限制或使用 WeakMap
- [ ] DOM 引用在元素移除后被清理
- [ ] 闭包中不保留不必要的外层变量