跳到主要内容

内存机制:栈/堆、按值/按引用、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 看可达性;泄漏是“不需要但仍可达”。