内存机制:栈/堆、按值/按引用、GC 的“可达性”是什么?为什么会内存泄漏?
面试速答(30 秒版 TL;DR)
- JS 数据通常分两类:原始类型(primitive)与对象类型(object)。
- 原始类型的值语义更接近“按值”;对象变量保存的是“引用”(指向堆中对象的地址/句柄)。
- 引擎用**栈(stack)管理执行上下文与局部变量引用;用堆(heap)**存放对象等动态分配数据。
- GC(垃圾回收)核心判定是可达性(reachability):从根对象(如全局对象、栈上的引用)出发不可达的对象可被回收。
- 内存泄漏本质:你“不再需要”的对象仍然可达(被某处引用着),GC 不敢回收。
心智模型:三个关键词
- 栈:调用栈上的上下文帧与局部引用(生命周期短,进出栈)
- 堆:对象实际内容(生命周期不确定)
- 可达性:GC 能否从根引用走到它
栈 vs 堆:用一句话说清
- 栈:更像“函数执行的工作台”,进出栈非常快。
- 堆:更像“对象仓库”,适合大小不定的数据结构。
按值 vs 按引用:面试口述版
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
const o1 = { x: 1 };
const o2 = o1;
o2.x = 2;
console.log(o1.x); // 2
解释要点:
a/b是两个独立的值(原始类型复制值)。o1/o2是两个引用指向同一个对象(复制的是引用)。
补充:即便某些实现会对小整数/短字符串做内部优化,面试回答仍按“值语义/引用语义”讲即可。
GC:为什么能“自动回收”
面试回答不要背算法细节,抓住“可达性”即可:
- 从根对象集合出发标记所有可达对象
- 清理未标记(不可达)对象
这也是为什么“泄漏”不等于“没 GC”,而是“仍可达”。
更深入的 GC 与泄漏排查见:
典型泄漏场景(面试必背)
- 全局变量意外持有:
window.xxx = ... - 闭包持有大对象:返回函数引用了不必要的数据
- 定时器/订阅未清理:
setInterval、事件监听、观察者、WebSocket - DOM 引用未释放:缓存了已移除节点,或在老代码里出现循环引用持有
典型题 & 标准答法
Q1:为什么“把对象设为 null”有时能释放内存?
因为你断开了引用链,使对象变成不可达,GC 才能在合适时机回收。
Q2:为什么闭包容易导致内存泄漏?
因为闭包让外层词法环境保持可达;如果在该环境里无意间保留了大对象引用,就可能长期无法回收。
易错点/坑
- “泄漏”不是立刻把内存用满,而是长期累积导致占用持续上升。
- GC 不是立即发生的;看到对象不可达也不代表立刻释放。
速记要点(可背诵)
- 栈管上下文,堆存对象;对象变量存引用。
- GC 看可达性;泄漏是“不需要但仍可达”。