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
它们之间的关系如下:
-
代码有问题 → 内存慢慢涨
-
内存快满 → 频繁 GC
-
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 种泄漏:
-
全局变量滥用:全局对象永远不会被回收;
-
闭包意外持有大对象
-
定时器 / 监听器没清理
-
Buffer 被缓存起来不释放:堆外内存也会泄漏
泄漏表现:
heapUsed一直涨不回落- 频繁 FullGC
- 越来越慢,最后 OOM
怎么看 Node 内存?
console.log(process.memoryUsage());
{
rss: 123456789, // 进程总占用内存(系统视角)
heapTotal: 9876543, // V8 堆总申请大小
heapUsed: 7654321, // ✅ JS 实际使用内存(GC 管这里)
external: 1234567 // ✅ 堆外内存(Buffer 等)
}
线上看内存,只要记住三句话:
-
heapUsed 平稳不持续上涨 = 健康
-
持续上涨不回落 = 内存泄漏
-
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 代码还在忙别的事,没空读。
操作系统就说:
没事,我先帮你存着。
这个 “帮你存着” 的地方,就是:接收缓冲区。
作用:
- 浏览器发数据,不管你 Node 忙不忙,内核先收下
- 存在接收缓冲区
- 等 Node 空闲了,再从内核读走(读到你代码里的 Buffer)
满了会怎样?
缓冲区满了 → 内核告诉浏览器:别发了,我装不下了→ 浏览器就会暂停发送(TCP 滑动窗口)
这就是流量控制。
发送缓冲区(Send Buffer) :操作系统帮你 排队要发出去的数据 的地方。
你 Node 代码要给浏览器返回一大段数据:“把这个 10MB 文件返回给前端!”
操作系统不会直接一股脑扔到网线上,而是:
- 先把数据放进 发送缓冲区
- 内核自己一点点通过网卡发出去
- 发成功一批,就从缓冲区删掉一批
作用:
- 让你的代码不用关心网络快慢
- 你只管往缓冲区扔数据
- 内核负责慢慢发、重传、保证可靠
满了会怎样?
你写得太快,内核发得太慢 →发送缓冲区满了 →你的 write() 操作会 阻塞 / 返回等待
这就是为什么高并发下会出现卡顿、backpressure。
2. Node.js 拿到的是什么?
Node 拿到的不是连接本身,而是一个 文件描述符(fd) ,一个数字,比如 fd=10。
你代码里的:
req.socketnet.Sockethttp请求里的连接对象
都只是对内核连接的引用,不是连接本体。
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。