javascript中对象实际数据存储在堆内存,栈中仅保存指向堆的引用;垃圾回收依据可达性判断对象是否存活,而非变量名存在与否。

对象在堆中分配,引用在栈中存储
JavaScript 创建对象时,实际数据(如属性、方法、嵌套对象)总是在堆内存中分配。栈内存只保存一个轻量级的引用(类似地址指针),指向堆中的具体位置。例如:
-
let user = { name: 'Alice', profile: { age: 30 } };→user变量本身在栈,而{ name: 'Alice', ... }和其内层对象都落在堆里 - 多次赋值
let u2 = user;不会复制堆中对象,只是让u2指向同一块堆内存
可达性决定对象是否存活
垃圾回收不看“有没有变量名”,而看“能不能从根访问到”。所谓根,包括:全局对象(window/global)、当前执行函数的局部变量、调用栈上的活动上下文、DOM 树的根节点等。只要存在一条引用链能抵达某个对象,它就被视为“可达”,不会被回收。
- 执行
user = null;后,若再无其他变量或属性引用该对象,它就变成不可达,下次 GC 周期可能被清理 - 闭包内部捕获的对象,即使外层函数已返回,只要闭包函数还活跃,那些对象仍可达
- 事件监听器未解绑、定时器回调持有对象引用,都会意外延长对象生命周期
面向对象场景中易被忽略的回收障碍
类和原型链本身不阻碍回收,但实例间的引用关系常引发隐式强持有:
-
父子引用循环:父类实例存子实例,子实例又通过
this.parent持有父引用 → 单靠引用计数会失效,现代引擎靠标记清除可处理,但仍建议主动断开 -
缓存未清理:类中维护静态 Map 缓存实例(如
class CacheManager { static map = new Map() }),若键是对象且未手动delete,对象将长期驻留堆中 -
监听器绑定在实例方法上:使用
obj.addEventListener('click', this.handleClick)时,若未用removeEventListener或绑定箭头函数/once,实例无法被回收
引擎如何分代优化回收效率
V8 等主流引擎采用分代回收策略,把堆分为新生代(Scavenge)和老年代(Mark-Sweep/Mark-Compact):
- 新创建的对象先进入新生代,多数短命对象在这里快速回收(复制算法,快但耗空间)
- 经历两次 GC 仍存活的对象,会被晋升到老年代;老年代对象多、生命周期长,用标记清除为主,辅以标记整理减少碎片
- 这意味着:频繁新建临时对象(如循环中创建配置项)压力主要在新生代;而长期存在的业务模型实例,一旦泄漏,影响更持久、排查更难
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











