nodejs 学习7:nodejs中的内存

5 阅读8分钟

GC 是什么

GC = Garbage Collection 垃圾回收

大部分后端语言(Java、Go、Node.js、Python 等)都自带 GC,程序运行时会创建很多对象,不用了就变成 “垃圾”,GC 负责把它们清理掉,释放内存。

正常情况下,GC 偶尔跑一次,几乎无感。

频繁的 GC,GC 跑得太勤、太久,CPU 大量被占用,业务逻辑被打断。

频繁 GC 的话,你会看到这些现象:

  • 接口响应突然变慢、超时变多

  • CPU 使用率居高不下

  • 日志里大量 GC 相关记录

  • 内存忽高忽低,一直降不下去

本质:CPU 都在 GC,没在执行业务代码

常见原因有以下几种:

  • 内存不够用:堆内存设置太小,刚创建对象就满了,GC 被迫一直跑。

  • 内存泄漏:本该回收的对象被一直引用着(比如缓存没清理、线程池没释放、全局 List 无限加数据),GC 清不掉,只能反复尝试。

  • 一次性加载太多数据:一次查几万、几十万条数据库数据到内存,直接撑爆

  • 缓存无限制增长:本地缓存、List、Map 无限往里塞,不淘汰、不清理,直接触发 GC

如果垃圾一直清理不掉,会怎么样呢?

就会OOM = Out Of Memory

  • 频繁 GC:拼命打扫,想腾出点内存
  • OOM:打扫到最后也没空间了,程序直接崩溃

就是系统告诉你:实在没内存可用了,我扛不住了,退出吧。

OOM 会发生什么?

  • 服务直接挂掉、重启

  • 接口疯狂报错、超时

  • 日志里出现类似:JavaScript heap out of memory

它们之间的关系如下:

  1. 代码有问题 → 内存慢慢涨

  2. 内存快满 → 频繁 GC

  3. GC 也回收不动 → OOM 崩溃

所以:看到频繁 GC,就是预警;一旦出现 OOM,就是已经炸了。

nodejs的内存管理

Node.js 的内存 = V8 堆内存 + 堆外内存

JS 代码能直接控制的只有V8 堆,GC 也只管这里。

其他内存(Buffer、Stream、TCP 缓冲区)GC 管不着,得靠你自己小心。

① V8 堆内存(Heap)—— 你写的 JS 都在这

存放:

  • 对象、数组、字符串
  • 闭包、作用域
  • 函数、原型
  • req/res 等对象

特点:

  • 由 V8 自动 GC 回收
  • 有默认上限(64 位默认约 2GB)
  • 内存满了 → 频繁 GC → 再满 → OOM 崩溃

V8 堆内部又分两块(GC 的核心):

新生代(New Space)—— 小对象、短命对象

  • 很小,默认 16~32MB
  • 存放临时变量、函数内临时对象
  • GC 跑得非常频繁(Minor GC)
  • 速度极快,几乎无感

老年代(Old Space)—— 活的久的对象

  • 很大,默认最大 1.5 ~ 2GB
  • 存放:全局对象、缓存、长生命周期对象
  • GC 跑得慢(Major GC / Full GC)
  • 一旦频繁 FullGC,服务就卡

V8 GC 是个清洁工,只做一件事:找出没人用的对象,清理,腾出空间。

怎么判断对象没人用?

很简单:

  • 从全局作用域出发
  • 顺着引用链找
  • 找不到的对象 = 垃圾
  • 找到的 = 还活着,不能删

对象怎么进入老年代?

在新生代熬过几次 GC 还没死 → 晋升到老年代。

所以:

  • 临时变量 → 新生代 → 很快被清
  • 全局缓存、长生命周期对象 → 老年代 → 很难清

② 堆外内存(Off-Heap)——GC 管不到

存放:

  • Buffer(大部分)
  • 文件流、TCP 连接
  • 内核缓冲区(收发缓冲区)
  • C++ 扩展模块

特点:

  • 不受 V8 GC 管理
  • 不占堆内存
  • 但会吃系统内存
  • 爆了一样 OOM

什么是内存泄漏?

本该被回收的对象,因为被意外引用,变成回收不了。

内存一直涨 → 涨满 → OOM。

最常见 4 种泄漏:

  1. 全局变量滥用:全局对象永远不会被回收;

  2. 闭包意外持有大对象

  3. 定时器 / 监听器没清理

  4. Buffer 被缓存起来不释放:堆外内存也会泄漏

泄漏表现:

  • heapUsed 一直涨不回落
  • 频繁 FullGC
  • 越来越慢,最后 OOM

怎么看 Node 内存?

console.log(process.memoryUsage());

{ 
    rss: 123456789, // 进程总占用内存(系统视角) 
    heapTotal: 9876543, // V8 堆总申请大小 
    heapUsed: 7654321, // ✅ JS 实际使用内存(GC 管这里) 
    external: 1234567 // ✅ 堆外内存(Buffer 等) 
}

线上看内存,只要记住三句话:

  1. heapUsed 平稳不持续上涨 = 健康

  2. 持续上涨不回落 = 内存泄漏

  3. external 异常大 = Buffer 没释放

什么是缓冲区 Buffer?

一块在内存里专门放二进制数据的临时空间,这个临时空间就是 Buffer

你可以把它理解成:

  • 前端里的 Array 是放数字、字符串的
  • Node 里的 Buffer 就是专门放字节的数组

它干的事就一个:在内存里先把数据存起来,攒够了 / 准备好了再一次性处理。

你去接水:

  • 水龙头一直滴水(数据流)
  • 你拿个先接着
  • 接满了再一次性倒进锅里

这个 就是 Buffer

  • 没有桶:水洒一地(数据丢了)
  • 有桶:攒一波再处理(高效、不丢失)

所以:Buffer = 内存里装二进制数据的桶

为什么要有 Buffer?

下面用一个TCP连接来讲清楚Buffer有什么作用。

前端发起请求 → 建立 TCP 连接 → 这个连接到底存在哪、由谁管

TCP 连接,既不存在前端,也不存在你的 Node 代码里,而是存在【操作系统内核】里。

你的 Node.js 只是向操作系统 “申请” 了一个连接的 “句柄” ,真正的连接队列、TCP 状态、缓冲区,全在内核。

你(浏览器)打电话给服务器(Node):

  • 电话线路接通 = TCP 连接
  • 这条线路不在你手机里,也不在对方手机里
  • 而在 电信运营商的交换机里(操作系统内核)

你和服务器只是各拿了一个 听筒 ,这个听筒在 Node 里就叫:socket(套接字)

1. 浏览器发起 TCP 连接

浏览器 → 网络 → 到达服务器网卡→ 交给 操作系统内核(Linux/Windows 内核)

内核做了这些事:

  • 建立 TCP 连接(三次握手)
  • 维护连接状态:ESTABLISHED、TIME_WAIT 等
  • 维护 接收缓冲区、发送缓冲区
  • 维护一个 连接表(tcp hash table)

这张连接表,就是所有 TCP 连接真正存放的地方。

接收缓冲区(Recv Buffer) :操作系统帮你 暂存收到的数据 的地方。

浏览器 → 网线 → 服务器网卡 → 内核,数据来了,但你的 Node 代码还在忙别的事,没空读。

操作系统就说:

没事,我先帮你存着。

这个 “帮你存着” 的地方,就是:接收缓冲区

作用:

  1. 浏览器发数据,不管你 Node 忙不忙,内核先收下
  2. 存在接收缓冲区
  3. 等 Node 空闲了,再从内核读走(读到你代码里的 Buffer)

满了会怎样?

缓冲区满了 → 内核告诉浏览器:别发了,我装不下了→ 浏览器就会暂停发送(TCP 滑动窗口)

这就是流量控制

发送缓冲区(Send Buffer) :操作系统帮你 排队要发出去的数据 的地方。

你 Node 代码要给浏览器返回一大段数据:“把这个 10MB 文件返回给前端!”

操作系统不会直接一股脑扔到网线上,而是:

  1. 先把数据放进 发送缓冲区
  2. 内核自己一点点通过网卡发出去
  3. 发成功一批,就从缓冲区删掉一批

作用:

  • 让你的代码不用关心网络快慢
  • 你只管往缓冲区扔数据
  • 内核负责慢慢发、重传、保证可靠

满了会怎样?

你写得太快,内核发得太慢 →发送缓冲区满了 →你的 write() 操作会 阻塞 / 返回等待

这就是为什么高并发下会出现卡顿、backpressure

2. Node.js 拿到的是什么?

Node 拿到的不是连接本身,而是一个 文件描述符(fd) ,一个数字,比如 fd=10

你代码里的:

  • req.socket
  • net.Socket
  • http 请求里的连接对象

都只是对内核连接的引用,不是连接本体。

Linux 里,一切皆文件。TCP 连接 = 一个文件。

文件描述符 = 连接的 “编号”。系统限制每个进程最多能开多少个编号,这就是 fd 上限。

在 Linux:

  • 默认每个进程最多开 1024 个 fd,可以改配置 ulimit -n 65535 开大一点。
  • 一个 TCP 连接占 1 个 fd
  • 连接达到 1024 个后
  • 新连接直接报错:Too many open files

这就叫 文件描述符耗尽

Buffer 和 OOM、GC 有什么关系?

TCP 连接本身 不占 V8 堆内存,所以不会触发 GC,但每个连接会占内核内存 + 少量 Node 描述符内存,连接太多会导致系统内存上涨,从而导致系统 OOM。