Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Node.js 与框架 / Node.js & Frameworks

1. Node.js 与后端框架学习路线 / Node.js and Backend Frameworks Learning Path

2. Node.js 运行时内部原理 / Node.js Runtime Internals

3. Node.js 异步模式与错误处理 / Async Patterns and Error Handling in Node.js

4. Express.js 深度剖析 / Express.js Deep Dive

5. Express 高级模式与生产实践 / Express Advanced Patterns and Production Practices

6. 现代 Node.js 框架对比 / Modern Node.js Framework Comparison

7. RESTful API 设计原则与实践 / RESTful API Design Principles and Practice

8. Node.js 环境配置与 12-Factor App / Environment Configuration and 12-Factor App

本页目录

Node.js 运行时内部原理 / Node.js Runtime Internals ​

📅 创建时间:2026-07-28 🏷️ 标签:#Nodejs #libuv #V8 #EventLoop #Stream #Buffer #Memory 📚 前置知识:[[00-overview]]


📋 本章目标 ​

  • 深入理解 libuv 事件循环的六个阶段及其执行时机
  • 掌握 V8 的 JIT 编译管线、隐藏类和垃圾回收策略
  • 理解 CJS 与 ESM 的模块解析算法及双包陷阱
  • 掌握 Buffer 的内存分配策略与安全考量
  • 理解 Stream 的背压机制、pipe 与 pipeline 的区别
  • 了解 EventEmitter 的同步执行模型与错误传播
  • 掌握 Worker Threads、Cluster 和 child_process 的进程管理能力
  • 学会使用堆快照和 Chrome DevTools 排查内存泄漏

第1部分:Event Loop 深度解析 ​

1.1 libuv 是什么 ​

libuv 是 Node.js 的跨平台异步 I/O 库,最初为 Node.js 开发,现在也被 Julia、Luvit 等项目使用。它提供统一的抽象层来封装不同操作系统的 I/O 多路复用机制:

┌─────────────────────────────────────────────────────────────┐
│                  libuv 跨平台抽象层                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              libuv Unified API                       │   │
│  │  uv_tcp_connect / uv_fs_read / uv_timer_start / ... │   │
│  └──────────┬──────────┬───────────┬───────────────────┘   │
│             │          │           │                        │
│      ┌──────┴────┐ ┌───┴───┐ ┌────┴──────┐                │
│      │  Linux    │ │ macOS │ │  Windows  │                │
│      │  epoll    │ │kqueue │ │   IOCP    │                │
│      └───────────┘ └───────┘ └───────────┘                │
│                                                             │
│  共同能力:                                                  │
│  • 非阻塞 TCP/UDP socket                                   │
│  • 异步文件系统操作(通过线程池)                           │
│  • 子进程管理 + 信号处理                                   │
│  • 定时器(timers)                                        │
│  • DNS 解析(c-ares 或系统 getaddrinfo)                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

1.2 事件循环的六个阶段 ​

┌─────────────────────────────────────────────────────────────┐
│                libuv 事件循环六阶段详解                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  进入循环 → 检查是否 alive(有活动句柄/请求)                │
│     │                                                       │
│     ├── 阶段1: timers                                       │
│     │   setTimeout / setInterval 到期回调                   │
│     │   注意:计时器在 poll 阶段更新,到期回调在此执行      │
│     │                                                       │
│     ├── 阶段2: pending callbacks                            │
│     │   上一轮延迟的系统回调(如 TCP 错误事件)             │
│     │   大多数情况此阶段为空                                 │
│     │                                                       │
│     ├── 阶段3: idle / prepare                               │
│     │   libuv 内部使用,开发者不应依赖                       │
│     │                                                       │
│     ├── 阶段4: poll(核心)                                  │
│     │   ┌─────────────────────────────────────────────┐    │
│     │   │ 1. 执行 poll 队列中的 I/O 回调               │    │
│     │   │ 2. 计算下一个 timer 的到期时间               │    │
│     │   │ 3. 如 poll 队列为空:                        │    │
│     │   │    • 有 setImmediate → 立即进入 check 阶段   │    │
│     │   │    • 无 setImmediate → 阻塞等待(超时时间    │    │
│     │   │      为最近的 timer 到期时间)                │    │
│     │   │    • 无 timer 则无限等待(等待 I/O 事件)    │    │
│     │   └─────────────────────────────────────────────┘    │
│     │                                                       │
│     ├── 阶段5: check                                        │
│     │   setImmediate 回调在此执行                           │
│     │                                                       │
│     └── 阶段6: close callbacks                              │
│         socket.on('close', ...) 等关闭事件                  │
│                                                             │
│  ⚡ 关键:每个阶段之间会清空 process.nextTick 和 microtask   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

1.3 微任务插入点 ​

事件循环每个阶段之间都有一条"检查线":先清空 process.nextTick 队列,再清空 microtask(Promise / queueMicrotask)队列,然后才进入下一个阶段。

typescript
// 经典面试题:输出顺序是什么?
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

const fs = require('fs');
fs.readFile(__filename, () => {
  // 此回调在 poll 阶段执行
  setTimeout(() => console.log('inner timeout'), 0);
  setImmediate(() => console.log('inner immediate'));
  process.nextTick(() => console.log('inner nextTick'));
});

console.log('sync');

// 输出:
// sync
// nextTick
// promise
// timeout 或 immediate(取决于首次进入 timers 时定时器是否到期)
// inner nextTick
// inner immediate
// inner timeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

关键洞察:在 I/O 回调内部(poll 阶段),setImmediate 永远先于 setTimeout(fn, 0) 执行。因为 I/O 回调执行完毕后,事件循环直接进入 check 阶段处理 setImmediate,然后才回到 timers 阶段。

1.4 线程池 ​

┌─────────────────────────────────────────────────────────────┐
│                  libuv 线程池                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  默认线程数:4(UV_THREADPOOL_SIZE 环境变量可调整)         │
│                                                             │
│  使用线程池的操作:                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • fs.* 文件系统操作(除 fs.FSWatcher)               │   │
│  │ • dns.lookup()(使用 getaddrinfo,阻塞式)           │   │
│  │ • crypto 中的 CPU 密集型操作(如 pbkdf2)            │   │
│  │ • zlib 压缩(线程池辅助)                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  不使用线程池(由 OS 内核异步处理):                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • TCP/UDP 网络 I/O(epoll/kqueue/IOCP)              │   │
│  │ • dns.resolve*()(c-ares,非阻塞 DNS)               │   │
│  │ • 信号事件 / 子进程 exit 事件                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  线程池瓶颈示例:                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ // 默认 4 线程,同时只能处理 4 个 fs 操作             │   │
│  │ // 第 5 个会被阻塞直到有空闲线程                       │   │
│  │ for (let i = 0; i < 8; i++) {                        │   │
│  │   fs.readFile('/large/file', (err, data) => {        │   │
│  │     console.log(`Done ${i}`);                        │   │
│  │   });                                                │   │
│  │ }                                                     │   │
│  │ // 前 4 个并行,后 4 个排队等待                        │   │
│  │                                                       │   │
│  │ // 解决方案:UV_THREADPOOL_SIZE=16 node app.js       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

1.5 阻塞 vs 非阻塞操作 ​

typescript
// ❌ 阻塞事件循环:同步操作
const data = fs.readFileSync('/large/file');          // 阻塞整个进程
const hash = crypto.pbkdf2Sync('pwd', 'salt', 100000, // 阻塞整个进程
                                64, 'sha512');

// ❌ 阻塞事件循环:长循环在单个 tick 中
function heavyComputation(n: number): number {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);  // 100 亿次仍在同一 tick
  }
  return result;             // 整个过程其他请求全部饥饿
}

// ✅ 非阻塞:异步版本
fs.readFile('/large/file', (err, data) => { /* I/O 不阻塞 */ });

// ✅ 拆分 CPU 密集任务:用 setImmediate 分段
function heavyComputationAsync(n: number): Promise<number> {
  return new Promise((resolve) => {
    let result = 0;
    let i = 0;
    function chunk() {
      const end = Math.min(i + 1000000, n);
      for (; i < end; i++) result += Math.sqrt(i);
      if (i < n) {
        setImmediate(chunk);  // 让出控制权给事件循环
      } else {
        resolve(result);
      }
    }
    chunk();
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

第2部分:V8 引擎内部原理 ​

2.1 JIT 编译管线 ​

V8 使用多层 JIT 编译策略,在启动速度和峰值性能之间取得平衡:

┌─────────────────────────────────────────────────────────────┐
│                  V8 JIT 编译管线                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  JavaScript 源码                                             │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Parser → 生成 AST                                   │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Ignition(解释器)                                   │   │
│  │  • 将 AST 编译为字节码                               │   │
│  │  • 立即开始执行,启动快                               │   │
│  │  • 收集类型反馈(Type Feedback)                      │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             │ 热点检测                       │
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  TurboFan(优化编译器)                               │   │
│  │  • 将热点字节码编译为高度优化的机器码                 │   │
│  │  • 利用类型反馈做推测性优化(Speculative Opt)       │   │
│  │  • 内联函数调用、消除冗余检查                        │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             │ 假设失效                       │
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  去优化(Deoptimization)                             │   │
│  │  • 类型假设被打破 → 丢弃优化代码                      │   │
│  │  • 回退到 Ignition 解释执行                          │   │
│  │  • 重新收集类型信息,可能再次优化                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  未来方向:Maglev(中间层编译器)                            │
│  • V8 9.5+  填补 Ignition 与 TurboFan 之间的空白            │
│  • 比 TurboFan 编译更快,生成的代码比 Ignition 更快         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

2.2 隐藏类(Hidden Classes)与内联缓存 ​

V8 为了提高属性访问速度,在幕后为结构相似的对象分配相同的"隐藏类"(Map)。当你以一致的顺序初始化对象属性时,它们共享同一个隐藏类,属性访问被优化为固定偏移量读取。当初始化顺序改变或动态添加属性时,隐藏类链分裂,属性访问退化为字典查找。

typescript
// ✅ 好的实践:一致的初始化顺序 → 共享隐藏类
function Point(x: number, y: number) {
  this.x = x;  // 隐藏类 C1
  this.y = y;  // 隐藏类 C2 (C1 → C2 变迁)
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// p1 和 p2 共享相同隐藏类,属性访问 O(1)

// ❌ 坏实践:不一致的初始化顺序 → 不同隐藏类
const a = { x: 1, y: 2 };  // 隐藏类链:{} → {x} → {x,y}
const b = {};
b.y = 2;                    // 隐藏类链:{} → {y}
b.x = 1;                    // 隐藏类链:{y} → {x,y}(不同于上面!)
// a 和 b 属性相同但隐藏类不同,无法共享内联缓存

// ❌ 删除属性也会导致隐藏类变化
delete a.x;  // 切换到字典模式,性能骤降
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

内联缓存(Inline Cache, IC)是 V8 在字节码中插入的优化桩:第一次访问属性时记录对象的隐藏类和偏移量,后续同一位置遇到相同隐藏类时直接使用缓存的偏移量,跳过查找过程。这就是为什么"单态"(一种类型)快、"多态"(2-4 种类型)尚可、"超态"(超过 4 种)退化。

2.3 去优化(Deoptimization)的常见触发条件 ​

typescript
// 触发去优化的典型场景:

// 1. 类型假设被打破
function add(a: number, b: number) {
  return a + b;
}
add(1, 2);      // V8 假设 a, b 都是 Smi(小整数)
add("hello", " world");  // 类型变了 → 去优化!

// 2. 超出隐藏类假设
function readX(obj: { x: number }) {
  return obj.x;  // IC 记录了 {x: number} 的隐藏类
}
readX({ x: 1 });
readX({ x: 2, y: 3 });   // 不同隐藏类,IC 变为多态
readX({ x: 3, y: 4, z: 5 }); // 继续退化
readX({ x: 4, w: 5 });   // 超态 → 放弃 IC 优化

// 3. 在优化代码中抛出异常(OSR 相关)
// 4. 使用 arguments 对象(在严格模式下会去优化)
// 5. try-catch 包裹的优化代码被触发

// ▶ 用 --trace-deopt 和 --trace-opt 观察优化/去优化:
// node --trace-deopt --trace-opt app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

2.4 垃圾回收:Orinoco ​

V8 的 GC 引擎代号 Orinoco,采用分代垃圾回收策略:

┌─────────────────────────────────────────────────────────────┐
│                  V8 分代垃圾回收                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  新生代(Young Generation / New Space)              │   │
│  │  ┌───────────────┬────────────────┐                 │   │
│  │  │  From-Space   │   To-Space     │                 │   │
│  │  │  (活跃对象)   │   (空闲)       │                 │   │
│  │  └───────────────┴────────────────┘                 │   │
│  │                                                     │   │
│  │  算法:Scavenger(Cheney's 半空间复制算法)          │   │
│  │  • 大小:通常 1-8 MB(因堆大小自适应)              │   │
│  │  • 触发:From-space 满时                             │   │
│  │  • 过程:将存活对象复制到 To-space,交换角色         │   │
│  │  • 存活 2 次的对象晋升到老生代                       │   │
│  │  • 特点:快但浪费一半空间                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                            │ 晋升                           │
│                            ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  老生代(Old Generation / Old Space)                │   │
│  │                                                     │   │
│  │  算法:Mark-Compact + Mark-Sweep                     │   │
│  │  • 标记阶段:从根对象出发标记所有可达对象            │   │
│  │  • 清除阶段:回收未标记对象的内存                    │   │
│  │  • 紧缩阶段:移动存活对象消除碎片(必要时)          │   │
│  │  • 并发标记(Concurrent Marking):不阻塞主线程      │   │
│  │  • 增量标记(Incremental Marking):分批标记         │   │
│  │  • 惰性清除(Lazy Sweeping):按需清除               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  其他空间:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • Large Object Space:> 页面大小的对象直接分配于此  │   │
│  │ • Code Space:JIT 编译的机器码                       │   │
│  │ • Map Space:隐藏类(Map 对象)                      │   │
│  │ • Read-Only Space:不可变对象(如字符串常量)        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

2.5 内存限制 ​

typescript
// 64 位系统默认限制:
// • 老生代最大约 1.4 GB(实际由 V8 按物理内存动态计算)
// • 新生代最大约 32 MB(64位)或 16 MB(32位)
// • 32 位系统老生代限制约 0.7 GB

// 调整老生代内存限制:
// node --max-old-space-size=4096 app.js   → 4 GB
// node --max-old-space-size=8192 app.js   → 8 GB

// 调整新生代内存限制:
// node --max-semi-space-size=64 app.js    → 64 MB

// 查看当前堆使用情况:
console.log(process.memoryUsage());
// {
//   rss: 49356800,        // 常驻集大小(物理内存)
//   heapTotal: 18268160,  // V8 已分配的堆内存
//   heapUsed: 6504720,    // V8 实际使用的堆内存
//   external: 49879,      // C++ 对象绑定的 JS 内存
//   arrayBuffers: 9386    // ArrayBuffer + SharedArrayBuffer
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第3部分:模块系统深度解析 ​

3.1 CJS require() 解析算法 ​

┌─────────────────────────────────────────────────────────────┐
│                require(X) 完整解析算法                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  require(X) 从 Y 路径的模块中调用:                          │
│                                                             │
│  1. X 是核心模块?                                           │
│     ├── YES → 返回核心模块(node:http, node:fs, ...)       │
│     └── NO  → 继续                                          │
│                                                             │
│  2. X 以 '/' 开头?                                          │
│     ├── YES → Y = 文件系统根目录                             │
│     └── NO  → 继续                                          │
│                                                             │
│  3. X 以 './' 或 '../' 或 '/' 开头?                        │
│     ├── YES → 直接文件查找(FILE_SEARCH)                    │
│     └── NO  → 进入 node_modules 遍历(DIR_SEARCH)          │
│                                                             │
│  DIR_SEARCH(包查找):                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 在 Y/node_modules 中查找 X                       │   │
│  │ 2. 未找到 → 在 Y/../node_modules 中查找 X           │   │
│  │ 3. 重复直到文件系统根目录                             │   │
│  │ 4. 仍未找到 → 检查全局目录 → 抛出 MODULE_NOT_FOUND  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  FILE_SEARCH(文件查找):                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. X 作为文件:X → X.js → X.json → X.node           │   │
│  │ 2. X 作为目录:X/package.json → X/index.{js,json,node}│  │
│  │ 3. 全部失败 → 抛出 MODULE_NOT_FOUND                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typescript
// 核心模块优先级最高,且不可被同名社区包覆盖
const http = require('http');          // 核心模块
const http2 = require('node:http');    // node: 前缀强制核心模块(推荐)

// require.cache —— 模块缓存
console.log(Object.keys(require.cache));
// ['/app/index.js', '/app/routes.js', ...]

// 删除缓存强制重新加载(慎用)
delete require.cache[require.resolve('./config')];
const freshConfig = require('./config');
1
2
3
4
5
6
7
8
9
10
11

3.2 ESM 解析与 package.json 字段 ​

jsonc
// package.json —— ESM 时代的入口控制
{
  "type": "module",  // .js 文件默认按 ESM 解析(省略时默认为 CJS)
  "main": "./dist/index.cjs",  // require() 入口(传统字段)

  // exports —— 现代入口定义(优先级高于 main)
  "exports": {
    ".": {
      "import": "./dist/index.mjs",       // import 'pkg'
      "require": "./dist/index.cjs",      // require('pkg')
      "types": "./dist/index.d.ts"        // TypeScript 类型
    },
    "./feature": {
      "import": "./dist/feature.mjs",
      "require": "./dist/feature.cjs"
    },
    "./package.json": "./package.json"     // 显式暴露以允许 import 自身
  },

  // imports —— 模块内路径别名(不以 '.' 开头,仅包内部可用)
  "imports": {
    "#utils": "./src/utils/index.js",
    "#utils/*": "./src/utils/*.js",
    "#config": "./src/config/${NODE_ENV}.js"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

3.3 双包(Dual Package)陷阱 ​

┌─────────────────────────────────────────────────────────────┐
│                  双包陷阱(Dual Package Hazard)              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:同一个包同时提供 CJS 和 ESM 版本                      │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ // 场景 A:CJS 消费者 require('dual-pkg')            │   │
│  │ const { foo } = require('dual-pkg');                 │   │
│  │ // 得到 CJS 副本                                      │   │
│  │                                                      │   │
│  │ // 场景 B:ESM 消费者 import 'dual-pkg'              │   │
│  │ import { foo } from 'dual-pkg';                      │   │
│  │ // 得到 ESM 副本                                      │   │
│  │                                                      │   │
│  │ // 如果 foo 内部维护状态(单例模式),CJS 和 ESM      │   │
│  │ // 各自加载了不同的副本 → 状态分裂!                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  解决方案:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 状态放在全局:globalThis.__SINGLETON__            │   │
│  │ 2. 仅提供 ESM,CJS 用包装层(ESM wrapper around CJS)│   │
│  │ 3. 使用 .cjs / .mjs 明确文件类型                     │   │
│  │ 4. exports 字段确保只能通过合法入口访问               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typescript
// module.createRequire() —— ESM 中使用 CJS require
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('some-cjs-package');  // 在 ESM 中安全加载 CJS

// 动态 import —— CJS 中使用 ESM
async function loadESM() {
  const { default: pkg } = await import('some-esm-package');
  return pkg;
}
1
2
3
4
5
6
7
8
9
10

第4部分:Buffer 与二进制数据 ​

4.1 Buffer 的类型体系 ​

┌─────────────────────────────────────────────────────────────┐
│                  二进制数据类型体系                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │               ArrayBuffer (ECMAScript)               │   │
│  │         固定长度的原始二进制数据缓冲区               │   │
│  │         .byteLength (字节数)                          │   │
│  └──────────┬────────────────────────────┬──────────────┘   │
│             │                            │                   │
│             ▼                            ▼                   │
│  ┌──────────────────────┐    ┌─────────────────────────┐   │
│  │  TypedArray 视图     │    │  DataView               │   │
│  │  Uint8Array          │    │  灵活读写不同字节序     │   │
│  │  Uint32Array         │    │  .getInt32(offset, le)  │   │
│  │  Float64Array        │    │  .setBigUint64(...)     │   │
│  │  (多种类型视图)      │    │  (单个 ArrayBuffer)     │   │
│  └──────────────────────┘    └─────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │       Buffer (Node.js,继承自 Uint8Array)            │   │
│  │  • Buffer.alloc / Buffer.allocUnsafe                 │   │
│  │  • 编码/解码:utf-8, base64, hex, latin1            │   │
│  │  • 字节序读写:readUInt32BE / writeDoubleLE         │   │
│  │  • Buffer.from(arrayBuffer) —— 零拷贝视图           │   │
│  │  • buf.buffer —— 访问底层 ArrayBuffer                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

4.2 内存分配与安全 ​

typescript
// ─── Buffer.alloc vs Buffer.allocUnsafe ──────────────────

// ✅ Buffer.alloc(size):分配并零填充
const safe = Buffer.alloc(1024);
// safe 中全是 0x00,不包含任何先前内存数据

// ⚠️ Buffer.allocUnsafe(size):分配但不初始化
const unsafe = Buffer.allocUnsafe(1024);
// unsafe 可能包含先前进程释放的敏感数据(密钥、密码...)
// 仅在立即完全覆盖时使用:
const buf = Buffer.allocUnsafe(1024);
buf.fill(data);  // 必须立即完全覆盖

// ❌ 危险模式:部分覆盖
const buf2 = Buffer.allocUnsafe(1024);
buf2.write('hello', 0);  // 只写了 5 字节,剩余 1019 字节是旧数据!

// ─── Buffer 池 (Buffer Pool) ────────────────────────────

// Node.js 维护一个 8 KB 的内部 Buffer 池
// Buffer.allocUnsafe(< 4KB) → 从池中切分(更快)
// Buffer.allocUnsafe(>= 4KB) → 独立分配
// Buffer.alloc() 始终独立分配 + 零填充

// ─── 零拷贝操作 ─────────────────────────────────────────

// 1. Buffer.from(arrayBuffer) — 共享底层内存
const ab = new ArrayBuffer(16);
const view = Buffer.from(ab);      // 零拷贝视图

// 2. buf.buffer — 访问底层 ArrayBuffer
const ab2 = buf.buffer;            // 可能是 SharedArrayBuffer

// 3. buf.slice() — 引用同一内存(零拷贝,但共享写入)
const slice = buf.slice(0, 10);    // 修改 slice 会影响 buf

// 4. buf.subarray() — 与 slice 类似但无参数复制
const sub = buf.subarray(0, 10);   // 推荐(语义更明确)

// ⚠️ Buffer.from(string) 会复制数据,非零拷贝
const copy = Buffer.from('hello', 'utf-8');  // 新分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

第5部分:Stream 内部原理 ​

5.1 Stream 类型体系 ​

┌─────────────────────────────────────────────────────────────┐
│                   Node.js Stream 类型全景                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    Readable                          │   │
│  │  • 数据来源:文件、网络、生成器                      │   │
│  │  • 两种模式:flowing / paused                        │   │
│  │  • 事件:data, readable, end, error, close          │   │
│  │  • 方法:read(), pipe(), resume(), pause()          │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             │                                │
│  ┌──────────────────────────┼──────────────────────────┐   │
│  │                          ▼                           │   │
│  │  ┌──────────────────────────────────────────────┐   │   │
│  │  │               Transform                       │   │   │
│  │  │  Readable + Writable 的继承组合                │   │   │
│  │  │  ._transform(chunk, encoding, callback)        │   │   │
│  │  │  zlib, crypto, 压缩/解压 流                   │   │   │
│  │  └──────────────────────────────────────────────┘   │   │
│  │                                                     │   │
│  │  ┌──────────────────────────────────────────────┐   │   │
│  │  │               Duplex                           │   │   │
│  │  │  独立可读 + 独立可写                            │   │   │
│  │  │  net.Socket, TLS 连接                          │   │   │
│  │  └──────────────────────────────────────────────┘   │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────────┘
│                             │                                │
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    Writable                          │   │
│  │  • 数据目标:文件、网络、HTTP 响应                   │   │
│  │  • 事件:drain, finish, error, close, pipe          │   │
│  │  • 方法:write(), end(), cork(), uncork()           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

5.2 背压(Backpressure)机制 ​

┌─────────────────────────────────────────────────────────────┐
│                   Stream 背压机制                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景:Readable 生产速度 > Writable 消费速度                 │
│                                                             │
│  Readable                          Writable                  │
│  ┌──────────┐                    ┌──────────────┐           │
│  │ 高速生产 │ ════ 数据流 ════→ │  低速消费     │           │
│  │ (磁盘    │                    │  (网络限速)   │           │
│  │  1GB/s)  │                    │  (1MB/s)      │           │
│  └──────────┘                    └──────────────┘           │
│                                                             │
│  机制:                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │  1. writable.write(chunk) 内部缓冲累积              │   │
│  │     ↓                                               │   │
│  │  2. 缓冲超过 highWaterMark(默认 16KB)              │   │
│  │     ↓                                               │   │
│  │  3. write() 返回 false → 信号!                     │   │
│  │     ↓                                               │   │
│  │  4. Readable 收到 false,暂停生产(pause)           │   │
│  │     ↓                                               │   │
│  │  5. Writable 消费完缓冲,清空内部队列                │   │
│  │     ↓                                               │   │
│  │  6. Writable 发出 'drain' 事件                      │   │
│  │     ↓                                               │   │
│  │  7. Readable 收到 'drain',恢复生产(resume)        │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typescript
import { Readable, Writable, pipeline } from 'stream';
import { createReadStream, createWriteStream } from 'fs';

// ─── 手动处理背压 ────────────────────────────────────────

function manualCopy(src: string, dest: string): void {
  const readable = createReadStream(src, { highWaterMark: 64 * 1024 });
  const writable = createWriteStream(dest);

  readable.on('data', (chunk: Buffer) => {
    const canContinue = writable.write(chunk);
    if (!canContinue) {
      readable.pause();                    // 背压:暂停读取
      writable.once('drain', () => {
        readable.resume();                 // 缓冲区清空,恢复读取
      });
    }
  });

  readable.on('end', () => writable.end());
}

// ─── pipe() 自动处理背压 ────────────────────────────────

// pipe() 内部实现了上述所有背压逻辑
createReadStream('src.txt').pipe(createWriteStream('dest.txt'));

// ─── pipeline() vs pipe() ───────────────────────────────

// pipe() 的问题:错误不会自动传播/销毁
createReadStream('src.txt')
  .pipe(transform)   // 如果 transform 报错,pipe 不会销毁 readable
  .pipe(createWriteStream('dest.txt'));
// → 可能造成内存泄漏(流未关闭)或文件句柄未释放

// pipeline() 的优势:自动错误传播、销毁和回调
import { pipeline } from 'stream/promises';

await pipeline(
  createReadStream('src.txt'),
  transform,
  createWriteStream('dest.txt'),
);
// → 任一流出错 → 所有流自动 destroy → Promise reject

// ─── 对象模式 (objectMode) ──────────────────────────────

const objectStream = new Readable({
  objectMode: true,    // 以对象为单位,而非 Buffer
  highWaterMark: 16,   // 最多缓冲 16 个对象
  read() {
    this.push({ id: 1, name: 'Alice' });
    this.push({ id: 2, name: 'Bob' });
    this.push(null);  // 结束
  }
});

// ─── Async Iterators 消费 Stream ────────────────────────
for await (const chunk of createReadStream('data.csv')) {
  // chunk 是 Buffer,逐块处理
  console.log(chunk.toString());
}

// ─── stream.compose() 与 stream.finished() ──────────────
import { compose, finished } from 'stream';

const pipeline = compose(transform1, transform2);

finished(pipeline, (err) => {
  if (err) console.error('Stream failed:', err);
  else console.log('Stream completed successfully');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

第6部分:EventEmitter 内部机制 ​

6.1 同步发射与异步陷阱 ​

typescript
import { EventEmitter, once, on } from 'events';

const emitter = new EventEmitter();

// ─── 核心行为:emit() 是同步的 ──────────────────────────

emitter.on('data', (payload) => {
  console.log('listener 1:', payload);
});
emitter.on('data', async (payload) => {
  await new Promise(r => setTimeout(r, 100));
  console.log('listener 2 (async):', payload);  // 这行在 emit 返回后才输出
});

console.log('before emit');
emitter.emit('data', { x: 1 });
console.log('after emit');

// 输出:
// before emit
// listener 1: { x: 1 }
// after emit          ← emit 不会等待 async listener!
// listener 2 (async): { x: 1 }

// ─── error 事件特殊行为 ─────────────────────────────────
// 如果 emit('error') 且没有 listener → 直接 throw,进程崩溃
emitter.emit('error', new Error('oops'));
// → Unhandled 'error' event → 进程退出

// ✅ 始终为 EventEmitter 注册 error 监听器
emitter.on('error', (err) => console.error('caught:', err));

// ─── EventEmitter.captureRejections ─────────────────────

const ee = new EventEmitter({ captureRejections: true });
ee.on('something', async () => {
  throw new Error('async rejection');  // 自动转发到 'error' 事件
});
ee.on('error', (err) => {
  console.log('captured async error:', err.message);
});
ee.emit('something');
// → captured async error: async rejection

// ─── 最大监听器警告 ─────────────────────────────────────
// 默认 maxListeners = 10
emitter.setMaxListeners(20);
// 设为 0 完全关闭限制(不推荐)
process.on('warning', (warn) => {
  if (warn.name === 'MaxListenersExceededWarning') {
    console.log('Too many listeners:', warn.message);
  }
});

// ─── events.once() 和 events.on() ──────────────────────

// Promise 风格:等待一次性事件
const [payload] = await once(emitter, 'ready');
console.log('Ready event received:', payload);

// AsyncIterator 风格:持续消费事件
for await (const [payload] of on(emitter, 'message')) {
  console.log('Message:', payload);
  if (payload.type === 'close') break;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

第7部分:进程管理 ​

7.1 process 对象精华 ​

typescript
// ─── 进程信息 ───────────────────────────────────────────
process.pid;                    // 进程 ID
process.ppid;                   // 父进程 ID
process.cwd();                  // 当前工作目录
process.arch;                   // CPU 架构 (x64, arm64)
process.platform;               // 平台 (linux, darwin, win32)
process.version;                // Node.js 版本
process.versions;               // 所有依赖版本 { node, v8, uv, ... }

// ─── 环境与参数 ─────────────────────────────────────────
process.env.NODE_ENV;           // 环境变量
process.argv;                   // [node路径, 脚本路径, ...参数]
// $ node app.js --port=3000 → ['node', 'app.js', '--port=3000']

// ─── 信号处理与优雅关闭 ─────────────────────────────────
const server = /* ... */;

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  server.close(() => {
    console.log('HTTP server closed');
    process.exit(0);
  });
  // 超时强制退出
  setTimeout(() => {
    console.error('Force exit after timeout');
    process.exit(1);
  }, 10000);
});

process.on('SIGINT',  () => { /* Ctrl+C */ });
process.on('uncaughtException', (err) => {
  console.error('Uncaught:', err);
  process.exitCode = 1;  // 设置退出码但不立即退出
});
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
});
process.on('beforeExit', () => {
  // 事件循环为空,即将退出前触发
  // 可以在这里做最后一次清理(但不再接受新任务)
});
process.on('exit', (code) => {
  // 进程即将退出,只能执行同步操作
  console.log(`Exit with code: ${code}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

7.2 child_process 四种模式 ​

┌─────────────────────────────────────────────────────────────┐
│                  child_process 四种模式                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ spawn(command, [args], [options])                    │   │
│  │ • 启动新进程,返回 ChildProcess(基于 Stream)       │   │
│  │ • 适合:长时间运行的进程、流式大数据                 │   │
│  │ • 输出通过 child.stdout / child.stderr Stream 获取  │   │
│  │ • 默认不创建 shell,跨平台兼容性好                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ exec(command, [options], callback)                   │   │
│  │ • 启动 shell 执行命令                                │   │
│  │ • 缓冲全部 stdout/stderr 到内存                      │   │
│  │ • 适合:短命令、输出小的场景                          │   │
│  │ • ⚠️ 输出过大可能导致内存溢出                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ execFile(file, [args], [options], callback)          │   │
│  │ • 直接执行可执行文件(不经过 shell)                  │   │
│  │ • 比 exec 更安全(无 shell 注入风险)                │   │
│  │ • Windows 上 .bat/.cmd 仍需 shell                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ fork(modulePath, [args], [options])                  │   │
│  │ • spawn 的特殊变体,专用于 Node.js 子进程            │   │
│  │ • 自动建立 IPC 通道(child.send / process.on('msg') │   │
│  │ • 隔离 V8 实例,每个子进程有独立堆                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typescript
import { spawn, exec, execFile, fork } from 'child_process';

// spawn —— 流式处理大输出
const child = spawn('ffmpeg', ['-i', 'input.mp4', '-c:v', 'libx264', 'output.mp4']);
child.stdout.on('data', (chunk) => process.stdout.write(chunk));
child.stderr.on('data', (chunk) => console.error(chunk.toString()));
child.on('close', (code) => console.log(`exit: ${code}`));

// exec —— 小命令
exec('ls -la /tmp', { maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
  if (err) return console.error(err);
  console.log(stdout);
});

// fork —— Node.js 专用
const worker = fork('./worker.js', ['--port=3001']);
worker.on('message', (msg) => console.log('from worker:', msg));
worker.send({ task: 'process', data: [1, 2, 3] });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

7.3 Worker Threads ​

┌─────────────────────────────────────────────────────────────┐
│                  Worker Threads 架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  主线程(Main Thread)                                │   │
│  │  • 运行事件循环                                       │   │
│  │  • 创建 Worker 实例                                   │   │
│  │  • postMessage() → 发送数据到 Worker                  │   │
│  │  • worker.on('message') → 接收 Worker 结果            │   │
│  └───────────┬──────────────────┬────────────────────────┘   │
│              │ MessagePort      │ SharedArrayBuffer          │
│              ▼                  ▼                            │
│  ┌──────────────────────┐ ┌────────────────────────────┐    │
│  │   Worker 1           │ │   Worker 2                 │    │
│  │  (独立 V8 实例)      │ │  (独立 V8 实例)            │    │
│  │  parentPort.post()   │ │  共享 SharedArrayBuffer    │    │
│  │  独立 heap (~1.4GB)  │ │  Atomics 同步原语         │    │
│  └──────────────────────┘ └────────────────────────────┘    │
│                                                             │
│  child_process.fork()  vs  worker_threads.Worker:           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ fork():      独立进程、独立内存、IPC 序列化开销大    │   │
│  │ Worker:      共享进程、独立 V8 堆、可共享内存        │   │
│  │              MessagePort 传输更快(结构化克隆 +       │   │
│  │              Transferable 零拷贝传输)               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typescript
// ─── Worker Threads 示例 ────────────────────────────────

// main.ts
import { Worker } from 'worker_threads';

function runHeavyTask(data: number[]): Promise<number> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      new URL('./worker.js', import.meta.url),
      { workerData: data }
    );
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped: ${code}`));
    });
  });
}

// worker.js
import { parentPort, workerData } from 'worker_threads';

const result = workerData.reduce((sum: number, n: number) => sum + Math.sqrt(n), 0);
parentPort?.postMessage(result);

// ─── SharedArrayBuffer + Atomics ─────────────────────────

// main.ts
const sab = new SharedArrayBuffer(4);  // 4 字节共享内存
const view = new Int32Array(sab);

const w1 = new Worker('./inc.js', { workerData: sab });
const w2 = new Worker('./inc.js', { workerData: sab });

// 两个 Worker 安全地并发递增同一个 Int32
// Atomics.add 保证原子性(无需锁)

// inc.js (worker)
import { workerData } from 'worker_threads';
const view = new Int32Array(workerData as SharedArrayBuffer);
Atomics.add(view, 0, 1);  // 原子递增,线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

7.4 Cluster 模块 ​

typescript
// ─── Cluster: 多进程 HTTP 服务 ──────────────────────────

import cluster from 'cluster';
import { createServer } from 'http';
import { availableParallelism } from 'os';

if (cluster.isPrimary) {
  const numCPUs = availableParallelism();
  console.log(`Primary ${process.pid} starting ${numCPUs} workers`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal ?? code})`);
    cluster.fork();  // 自动重启
  });

  // 零停机重启(逐个重启 worker)
  process.on('SIGUSR2', () => {
    const workers = Object.values(cluster.workers!);
    let i = 0;
    function restartNext() {
      const worker = workers[i++];
      if (!worker) return;
      worker.disconnect();
      worker.on('exit', () => {
        cluster.fork();
        setTimeout(restartNext, 1000);  // 等 1s 让新 worker 就绪
      });
    }
    restartNext();
  });

} else {
  // 每个 Worker 监听同一个端口(由 cluster 模块实现 socket 共享)
  createServer((req, res) => {
    res.writeHead(200);
    res.end(`Hello from worker ${process.pid}`);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

第8部分:内存管理与泄漏排查 ​

8.1 堆结构全景 ​

┌─────────────────────────────────────────────────────────────┐
│                  V8 堆内存结构                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  New Space(新生代)             1 - 8 MB            │   │
│  │  ┌────────────────┬────────────────┐                │   │
│  │  │   From-Space   │   To-Space     │   Semispace    │   │
│  │  │   活跃对象     │   空闲(备用) │                │   │
│  │  └────────────────┴────────────────┘                │   │
│  │  短命对象(如局部变量),Scavenger 快速回收          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Old Space(老生代)             ~1.4 GB (默认)      │   │
│  │  长命对象(闭包、全局变量、缓存),Mark-Compact 回收 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Large Object Space              无固定大小          │   │
│  │  大于页面大小(通常 256KB)的对象                    │   │
│  │  直接分配于此,避免新生代复制开销                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Code Space                       无固定大小          │   │
│  │  JIT 编译后的机器码                                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Map Space                        无固定大小          │   │
│  │  隐藏类(Map / Shape)对象                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

8.2 GC 策略与触发条件 ​

typescript
// ─── Scavenger(新生代 GC / Minor GC)────────────────────
// 触发条件:From-space 被填满
// 特点:Stop-the-world,但极快(< 1ms 通常)
// 频率:高(可能每秒多次)

// ─── Mark-Compact(老生代 GC / Major GC)────────────────
// 触发条件(任一):
// 1. 老生代空间即将耗尽
// 2. 晋升对象太多导致老生代增长过快
// 3. 增量标记完成后发现需要 compact
//
// 特点:
// • 并发标记(Concurrent Marking)— 不阻塞主线程
// • 增量标记(Incremental Marking)— 分批执行
// • Stop-the-world 仅在 final pause 阶段

// ─── 观察 GC 行为 ──────────────────────────────────────
// node --trace-gc app.js
// node --expose-gc app.js → 在代码中调用 global.gc()(手动触发)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

8.3 常见内存泄漏模式 ​

typescript
// ❌ 模式1:全局变量累积
const cache = new Map();  // 全局缓存从不清理
app.get('/data/:id', (req, res) => {
  cache.set(req.params.id, fetchData(req.params.id)); // 无限增长!
});

// ✅ 修复:使用 LRU Cache
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 500, ttl: 1000 * 60 * 5 });

// ❌ 模式2:遗忘的定时器
class Service {
  private interval: NodeJS.Timeout;
  start() {
    this.interval = setInterval(() => this.poll(), 1000);
    // 如果 Service 实例没有被正确销毁,定时器永远运行
  }
  stop() { clearInterval(this.interval); }  // 必须有!
}

// ❌ 模式3:未关闭的 Stream / 连接
async function processFile(path: string) {
  const stream = createReadStream(path);
  for await (const chunk of stream) {
    // 处理 chunk
  }
  // 如果提前 break/return,stream 可能未关闭
  // ✅ 始终 try-finally 调用 stream.destroy()
}

// ❌ 模式4:闭包持有大对象引用
function createProcessor() {
  const largeData = Buffer.alloc(100 * 1024 * 1024); // 100 MB
  return function process(item: string) {
    // 只用了 item,但闭包持有整个 largeData
    return item.toUpperCase();
  };
}
const processor = createProcessor();
// largeData 永远不会被 GC,因为 processor 持有对其作用域的引用

// ✅ 释放:将 largeData = null 在使用完毕后

// ❌ 模式5:EventEmitter 监听器泄漏
class MyService extends EventEmitter {}
const svc = new MyService();
for (let i = 0; i < 20; i++) {
  svc.on('update', () => {});  // 20 个监听器,但 svc 不被 GC
}
// 如果 svc 本身是长生命周期对象,监听器会累积

// ✅ 用 .once() 或及时 .removeListener()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

8.4 内存泄漏排查流程 ​

typescript
// ─── 方法1:heap snapshot(堆快照)──────────────────────

// 启动时带 --inspect:
// node --inspect app.js

// 在 Chrome DevTools (chrome://inspect) 中:
// 1. Memory → Take heap snapshot(启动初期)
// 2. 运行一段时间 / 施加负载
// 3. Take heap snapshot(问题期)
// 4. 选择 Comparison 视图 → 按 Delta 排序
// 5. 找到增长最多的对象类型
// 6. 追溯 Retainer 链 → 找到导致泄漏的引用路径

// ─── 方法2:Allocation Timeline ────────────────────────

// 在 DevTools Memory Tab:
// 1. 选择 "Record allocation timeline"
// 2. 开始记录
// 3. 运行负载
// 4. 停止记录
// 5. 观察蓝色柱状图(存活内存)是否持续上升不回落

// ─── 方法3:Allocation Sampling ─────────────────────────

// 在 DevTools Memory Tab:
// 1. 选择 "Allocation sampling"
// 2. 开始采样
// 3. 查看按函数/文件分组的分配量
// 4. 找到分配最多内存的函数 → 可能是泄漏源

// ─── 方法4:关键指标监控 ─────────────────────────────────
setInterval(() => {
  const { rss, heapTotal, heapUsed, external, arrayBuffers } =
    process.memoryUsage();
  console.log({
    rss: `${(rss / 1024 / 1024).toFixed(1)} MB`,
    heapUsed: `${(heapUsed / 1024 / 1024).toFixed(1)} MB`,
    heapTotal: `${(heapTotal / 1024 / 1024).toFixed(1)} MB`,
    external: `${(external / 1024 / 1024).toFixed(1)} MB`,
  });
  // 如果 heapUsed 只升不降 → 潜在泄漏
}, 5000);

// Runtime flags:
// --max-old-space-size=4096  → 增大堆限制(治标)
// --expose-gc               → 暴露 global.gc()
// --trace-gc                → 观察每次 GC 发生
// --inspect                 → 连接 DevTools
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

核心总结 ​

总结1:事件循环是 Node.js 的心脏 ​

Node.js 的性能特征完全由事件循环决定。理解六个阶段的执行顺序、微任务插入点和线程池的角色,是写出高性能 Node.js 代码的前提。记住:除了 thread pool 中的操作和 Worker Threads,所有 JavaScript 代码都在一个线程上执行。阻塞事件循环 = 阻塞整个服务。

总结2:V8 优化是"推测性"的 ​

V8 的 JIT 编译建立在类型推测之上——它根据已观察到的行为生成优化代码,如果假设被打破就去优化。写"V8 友好"的代码意味着:保持一致的参数类型、一致的属性初始化顺序、避免动态增删属性。用 --trace-deopt 可以看到哪些优化的假设被打破了。

总结3:Stream 优于 Buffer 用于大数据 ​

对于超过内存承载能力的数据,Stream 是唯一正确的选择。背压机制保证了生产者和消费者之间的平衡,pipeline() 优于 pipe() 因为它正确处理错误传播和流清理。日常开发中优先使用 pipeline() 和 async iterator。

总结4:内存泄漏排查需要系统化 ​

堆快照对比 + Retainer 链路追溯是最可靠的方式。常见泄漏模式(全局缓存、遗忘定时器、未关闭流、闭包闭包)需要刻入肌肉记忆。生产环境中持续监控 process.memoryUsage() 的 heapUsed 趋势,设置合理的内存限制和优雅重启策略。


章节测试 ​

测试1:libuv 事件循环的 poll 阶段主要负责什么? ​

A. 执行 setTimeout 回调 B. 执行 setImmediate 回调 C. 执行 I/O 回调并在队列为空时阻塞等待新事件 D. 执行 process.nextTick 回调

测试2:以下关于 V8 隐藏类的说法哪个是正确的? ​

A. 属性初始化的顺序不影响隐藏类 B. 使用 delete 删除属性不会影响隐藏类 C. 结构相同但属性添加顺序不同的对象会得到不同的隐藏类 D. 隐藏类是 ES2025 规范要求的特性

测试3:CJS 的 require() 解析模块时,核心模块的优先级是什么? ​

A. 低于 node_modules B. 等于 node_modules C. 最高优先级,不能被打包覆盖 D. 取决于 package.json 的 main 字段

测试4:Buffer.allocUnsafe() 的主要风险是什么? ​

A. 分配失败率高 B. 包含先前内存中的敏感数据 C. 比 Buffer.alloc() 更慢 D. 不支持 UTF-8 编码

测试5:stream.pipeline() 相比 stream.pipe() 的主要优势是什么? ​

测试6:Worker Threads 和 child_process.fork() 的核心区别是什么? ​

测试7:如何用 Chrome DevTools 定位 Node.js 的内存泄漏? ​


参考答案 ​

测试1答案 ​

答案:C。poll 阶段是事件循环的核心阶段——它执行 I/O 回调,当 poll 队列为空时,如果有 setImmediate 则进入 check 阶段,否则计算最近的 timer 超时时间并阻塞等待。setTimeout 在 timers 阶段,setImmediate 在 check 阶段,process.nextTick 在阶段间隙执行。

测试2答案 ​

答案:C。V8 的隐藏类(Map)对属性添加顺序敏感——即使最终属性集合相同,添加顺序不同也会产生不同的隐藏类。A 错误(顺序不同隐藏类不同),B 错误(delete 会导致退化为字典模式),D 错误(隐藏类是 V8 的实现细节不是规范)。

测试3答案 ​

答案:C。核心模块(如 http, fs, path)的优先级最高,位于所有 node_modules 包之前。即使你安装了一个叫 http 的 npm 包,require('http') 仍然返回核心模块。使用 require('node:http') 前缀可以让意图更明确。

测试4答案 ​

答案:B。Buffer.allocUnsafe() 不初始化内存,可能包含先前释放的进程内存中的敏感数据(密钥、密码、token 等)。如果这些数据被意外泄露(例如通过 HTTP 响应),就是严重的安全漏洞。仅在立即完全覆盖所有字节时使用 allocUnsafe。

测试5答案 ​

pipeline() 自动处理三个关键问题:(1) 错误传播——任一流出错时自动销毁所有流,(2) 回调/异步支持——支持 Promise 和 callback,(3) 完成通知——所有数据完成传输后才回调。而 pipe() 不会在源流出错时自动销毁目标流,也不会传播目标流的错误到源流,容易造成资源泄漏。

测试6答案 ​

Worker Threads 在同一进程内运行,共享进程内存但有独立的 V8 实例(独立堆),可以通过 SharedArrayBuffer 零拷贝共享内存,MessageChannel 传输也更快。child_process.fork() 创建完全独立的进程,有独立的内存空间,IPC 需要序列化/反序列化,开销更大但隔离性更强。选择原则:需要共享内存和极致性能用 Worker Threads,需要强隔离、独立崩溃域用 fork/cluster。

测试7答案 ​

使用 node --inspect app.js 启动应用,在 Chrome DevTools (chrome://inspect) 的 Memory Tab 中:(1) 使用 Heap Snapshot 对比——在负载前和负载后各拍一次快照,通过 Comparison 视图按 Delta 排序找到增长最多的对象类型,追溯 Retainer 引用链;(2) 使用 Allocation Timeline 观察是否持续上升不回落;(3) 使用 Allocation Sampling 按函数分组查看分配热点。辅以 process.memoryUsage() 持续监控 heapUsed 趋势。


相关笔记 ​

  • [[00-overview]] — Node.js 运行时全景与框架选型
  • [[../01-javascript-and-typescript/03-async-and-event-loop]] — JS 事件循环与异步模式基础
  • [[02-async-patterns-and-error-handling]] — Node.js 异步模式与错误处理
  • [[05-modern-node-frameworks]] — Fastify, Hono, NestJS 对比
  • stream 官方指南:nodejs.org/en/docs/guides/backpressuring-in-streams
  • V8 博客:v8.dev/blog

下一步学习 ​

  • [ ] 用 node --trace-gc --trace-deopt 运行一个实际项目,观察 V8 的优化行为
  • [ ] 编写一个使用 pipeline() 的大文件处理脚本,体验背压机制
  • [ ] 用 Worker Threads 实现一个并行 CPU 计算任务
  • [ ] 用 Chrome DevTools --inspect 连接本地 Node 服务,拍一次堆快照
  • [ ] 阅读 异步模式与错误处理 掌握 Node.js 中的错误处理最佳实践

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. Node.js 与后端框架学习路线 / Node.js and Backend Frameworks Learning Path
下一篇3. Node.js 异步模式与错误处理 / Async Patterns and Error Handling in Node.js

持续记录,持续成长

Copyright © Tidenflow