前端analysis | 知其所以然

js 堆栈

2025-07-03


🧠 一、为何要有 栈内存非堆内存(包括代码空间、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 提供了强大的内存分析功能,主要用于:

  • 查找内存泄漏
  • 分析对象生命周期
  • 监控垃圾回收行为

📌 打开方式

  1. 打开 DevTools(F12Ctrl + Shift + I
  2. 切换到 Memory 面板

📌 三种快照工具的作用

工具 功能
Heap snapshot 静态快照,显示某一时刻所有内存中的对象和引用关系
Allocation instrumentation on timeline 记录分配过程,查看哪些对象持续存在,适合定位内存泄漏
Allocation profiler 显示内存分配的堆栈,帮助你理解哪些代码创建了对象

🧪 使用技巧(经典排查流程)

✅ 1. 分析内存泄漏(快照对比法)

1
2
3
① 打开页面 → Snapshot1
② 执行操作 → Snapshot2
③ 再清理操作(如返回、解绑)→ Snapshot3
  • 对比 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
2
3
4
5
6
7
8
9
10
11
function foo() {
let a = 10;
bar();
}

function bar() {
let b = 20;
console.log(b);
}

foo();

🧱 执行步骤与调用栈状态变化:

  1. 初始状态

    1
    [Global Execution Context]   // 全局上下文入栈
  2. 调用 foo()

    1
    2
    [foo Execution Context]      // foo 调用帧压栈
    [Global Execution Context]
  3. foo 中调用 bar()

    1
    2
    3
    [bar Execution Context]      // bar 调用帧压栈
    [foo Execution Context]
    [Global Execution Context]
  4. console.log 执行完毕,bar() 结束

    1
    2
    [foo Execution Context]      // bar 出栈
    [Global Execution Context]
  5. foo() 结束

    1
    [Global Execution Context]
  6. 脚本执行结束,栈清空

    1
    []

🎯 三、作用域链也依赖“执行上下文”栈结构

  • 每个执行上下文都有一个作用域链(scope chain)指针。
  • JS 在查找变量时,先在当前帧找,找不到就向“外层上下文”继续查找,直到全局。

🧠 总结

栈中内容 说明
Call Stack(调用栈) 管理函数调用流程,每次调用函数就压栈,函数返回就出栈
执行上下文(Execution Context) 每个函数调用的“帧”,包含作用域、变量、参数、this
作用域链指针 每个上下文记录其外部上下文,构成作用域链

如果你想深入了解如闭包变量是如何在栈与堆之间共享的递归调用导致的栈溢出原理、或如何用 DevTools 可视化这些调用栈,我可以给你具体例子或图解。需要吗?

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏