🧠 一、为何要有 栈内存 和 非堆内存(包括代码空间、MapSpace 等)?
📌 1. 栈内存 vs 堆内存:性能与功能的权衡
特性 | 栈内存(Stack) | 堆内存(Heap) |
---|---|---|
分配速度 | 非常快(指针移动) | 相对较慢(需要查找空闲内存) |
生命周期 | 函数调用期内(自动回收) | 不确定(取决于引用和 GC) |
存储类型 | 原始值、函数上下文、局部引用 | 对象、数组、闭包、函数体等复杂结构 |
大小限制 | 较小(通常几十 KB ~ 几 MB) | 大得多(老生代最大可设 GB 级) |
管理方式 | 由编译器/运行时自动管理,顺序存取 | 由垃圾回收器管理,非连续结构 |
👉 为什么需要栈?
- 快速响应函数调用(如递归、作用域压栈)
- 更节省内存,临时变量很快被释放
👉 为什么需要堆?
- 存放动态分配和生命周期不确定的对象
- 如闭包对象、事件监听器、DOM 节点引用等
📌 2. 为什么还需要“非堆内存”空间(如 Code Space、ReadOnly Space)?
V8 除了“普通堆”,还有很多特殊用途的内存区间:
空间名称 | 作用与意义 |
---|---|
Code Space | 存储 JS 编译后的字节码 / 机器码。为了性能优化和执行权限控制,需要特殊对待。 |
Map Space | 存储对象的隐藏类(Hidden Class)结构,支持快速对象访问。 |
ReadOnly Space | 存不可变结构,如 NaN , Infinity ,多线程共享更安全。 |
Large Object Space | 避免复制大对象,直接分配,提高效率。 |
✅ 这些空间本质上是为了安全性、性能、内存利用率最大化设计的。
🛠️ 二、Chrome DevTools → Memory 面板使用技巧
Chrome DevTools 提供了强大的内存分析功能,主要用于:
- 查找内存泄漏
- 分析对象生命周期
- 监控垃圾回收行为
📌 打开方式
- 打开 DevTools(
F12
或Ctrl + Shift + I
) - 切换到 Memory 面板
📌 三种快照工具的作用
工具 | 功能 |
---|---|
Heap snapshot | 静态快照,显示某一时刻所有内存中的对象和引用关系 |
Allocation instrumentation on timeline | 记录分配过程,查看哪些对象持续存在,适合定位内存泄漏 |
Allocation profiler | 显示内存分配的堆栈,帮助你理解哪些代码创建了对象 |
🧪 使用技巧(经典排查流程)
✅ 1. 分析内存泄漏(快照对比法)
1 | ① 打开页面 → Snapshot1 |
- 对比 Snapshot2 和 Snapshot3,看是否有对象未被回收
- 用 “Object retained by” 找出哪个对象引用了它
✅ 2. 查看 Detached DOM 节点
- 搜索关键字:
Detached
,表示已脱离 DOM 树但仍在 JS 引用中的元素 - 这类对象非常容易导致内存泄漏(如事件未解绑)
✅ 3. 观察“retainers”(引用链)
- 在某个对象上右键 →
Reveal in Summary
- 查看其被哪些对象引用
- 找到 GC 无法释放的根引用(通常是全局变量、闭包、缓存)
✅ 4. 使用 Timeline 模式分析增长
- 点击
Allocation instrumentation on timeline
- 执行交互操作
- 观察内存是否持续增长,是否 GC 无法回收
📘 小技巧列表
技巧 | 说明 |
---|---|
window.leak = someObject |
主动把对象挂到全局,帮助你调试和快照标记 |
分配对象时添加 className 或 custom ID | 帮助在 heap snapshot 中识别 |
用 console.profile() /profileEnd() |
结合 Performance 分析内存或执行瓶颈 |
开启 GC 日志(Node) | --trace-gc 、--inspect 等可用于服务端调试 |
✅ 总结
概念 | 作用 |
---|---|
栈内存 | 快速处理函数调用、临时变量,生命周期短,性能高 |
堆内存 | 动态分配复杂数据结构,对象生命周期受引用控制 |
非堆空间 | 存机器码、Map 结构、只读常量等,引擎层级优化需要 |
DevTools 技巧 | 快照比对 + Timeline 观察 + Retainer 链分析是三大绝招 |
是的,完全正确!在 JavaScript 中,栈不仅仅用于存储原始数据类型,还承担着非常核心的职责:管理函数的执行过程。我们来具体解释一下这句话:
✅ 一、栈中包含的内容
在 JavaScript 中,所谓“栈”主要是指调用栈(Call Stack),这是 JS 引擎(如 V8)在运行时用来追踪函数调用的结构。
📦 调用栈中每一层都叫一个“执行上下文(Execution Context)”,它包含:
组成部分 | 说明 |
---|---|
函数参数 | 当前函数调用时传入的参数 |
局部变量 | 在函数内部声明的变量(let , const , var ) |
this 绑定 |
当前执行环境的 this 值(根据调用方式不同而变化) |
外部引用环境指针 | 指向当前上下文的外部作用域,用于作用域链([[Scope]] ) |
函数调用状态 | 调用位置、是否暂停等状态信息 |
✅ 这些内容整体就是函数的“调用帧”(Call Frame),每当你调用一个函数,JS 引擎就把这个帧压到调用栈顶部。
🔁 二、函数调用过程中的调用栈演化示例
1 | function foo() { |
🧱 执行步骤与调用栈状态变化:
初始状态
1
[Global Execution Context] // 全局上下文入栈
调用 foo()
1
2[foo Execution Context] // foo 调用帧压栈
[Global Execution Context]foo 中调用 bar()
1
2
3[bar Execution Context] // bar 调用帧压栈
[foo Execution Context]
[Global Execution Context]console.log 执行完毕,bar() 结束
1
2[foo Execution Context] // bar 出栈
[Global Execution Context]foo() 结束
1
[Global Execution Context]
脚本执行结束,栈清空
1
[]
🎯 三、作用域链也依赖“执行上下文”栈结构
- 每个执行上下文都有一个作用域链(scope chain)指针。
- JS 在查找变量时,先在当前帧找,找不到就向“外层上下文”继续查找,直到全局。
🧠 总结
栈中内容 | 说明 |
---|---|
Call Stack(调用栈) | 管理函数调用流程,每次调用函数就压栈,函数返回就出栈 |
执行上下文(Execution Context) | 每个函数调用的“帧”,包含作用域、变量、参数、this 等 |
作用域链指针 | 每个上下文记录其外部上下文,构成作用域链 |
如果你想深入了解如闭包变量是如何在栈与堆之间共享的、递归调用导致的栈溢出原理、或如何用 DevTools 可视化这些调用栈,我可以给你具体例子或图解。需要吗?
赏
使用支付宝打赏
使用微信打赏
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏