所属板块:1. 数据类型与内存机制
记录日期:2026-03-xx
更新:遇到新内存问题时补充
1. 栈内存(Stack) vs 堆内存(Heap) 快速对比
| 方面 | 栈内存(Stack) | 堆内存(Heap) |
|---|---|---|
| 存储内容 | 基本类型值 + 引用类型的地址指针 | 引用类型对象的实际内容(对象、数组等) |
| 大小 | 较小、固定(通常几MB) | 较大、动态扩展 |
| 分配/释放 | 函数调用时自动压栈,结束时弹出 | 由垃圾回收器(GC)管理 |
| 访问速度 | 极快(连续内存) | 稍慢(指针寻址) |
| 生命周期 | 与函数执行上下文绑定 | 只要有引用存在就不会被回收 |
| 典型例子 | let a = 100; let fn = () => {} | let obj = {}; let arr = [1,2,3] |
记住:栈负责“快速存取 + 自动管理”,堆负责“存放大块、可变数据 + 手动(GC)回收”。
2. V8 引擎的垃圾回收机制(主要浏览器 & Node.js 用的引擎)
V8 采用分代回收策略,把对象分为:
-
新生代(Young Generation):存活时间短的对象,大部分对象在这里被快速回收
- 空间较小(几MB ~ 几十MB)
- 使用 Scavenge 算法(复制算法):From 区 → To 区复制存活对象,交换角色,清空旧区
- 回收速度非常快,但会浪费一半空间
-
老生代(Old Generation):经过多次新生代回收仍存活的对象
- 空间较大(几百MB ~ GB级)
- 使用 标记-清除(Mark-Sweep) + 标记-整理(Mark-Compact)
- 标记阶段:从根(全局变量、栈引用等)开始遍历,标记可达对象
- 清除阶段:清除未标记的(垃圾)
- 整理阶段(可选):把存活对象往前挪,减少碎片
额外优化:
- 增量标记(Incremental Marking):把标记拆分成小块,避免长时间卡顿
- 并发标记(Concurrent Marking):部分工作放到后台线程
3. 内存泄漏的 4 种最常见场景(自己项目里最容易踩)
-
意外创建的全局变量
function foo() { bar = "意外全局变量"; // 没有 var/let/const → 挂到 window 上 }解决:严格模式("use strict")会报错;养成声明变量习惯。
-
被遗忘的定时器或事件监听
setInterval(() => { // 定时器里引用了 dom 或大对象,没清理 }, 1000); // 或 element.addEventListener('click', handler); // 组件卸载后没 remove解决:在组件卸载时 clearInterval / removeEventListener。
-
脱离 DOM 的引用仍存在
let detached = document.getElementById('box'); document.body.removeChild(detached); // DOM 没了,但 detached 还持有引用 // → 整个 DOM 树无法回收解决:移除引用 detached = null;
-
闭包滥用导致外部变量长期被引用
function createLeak() { let bigData = new Array(10000000).fill('x'); return function() { console.log(bigData.length); // 闭包一直引用 bigData }; } const fn = createLeak(); // fn 存在一天,bigData 就泄漏一天解决:只在必要时使用闭包;用完后置 null(较少用)。
4. 快速检测内存泄漏的小技巧(Chrome DevTools)
- Performance 面板 → 录制一段时间 → 看内存曲线是否持续上升
- Memory 面板 → Heap Snapshot → 多次快照对比,看哪些对象持续增长
- 关注 Retained Size 大的对象,尤其是 Detached DOM tree
5. 小结 & 日常注意
- 基本类型几乎不会泄漏(栈自动管理)
- 引用类型 + 闭包 + 定时器 + DOM 操作 是泄漏重灾区
- 写完复杂组件/长生命周期函数后,习惯问自己:“这些引用会被及时释放吗?”
- 现代框架(React/Vue)帮我们处理了很多,但自定义缓存、大数据处理仍需小心
下一篇文章会记录浅拷贝与深拷贝的实现方式,以及手写带循环引用处理的深拷贝(用 WeakMap)。
返回总目录:戳这里