流是 Node.js 处理数据的分片式、非阻塞 I/O 机制,核心是把数据拆成一小块(chunk)逐段传输,而不是一次性全加载到内存。 它基于 EventEmitter 实现,可读流负责生产数据,通过 data 事件把块推出去;可写流负责消费数据,写入缓冲区再输出。 流会自动处理读写速度不匹配的问题,不会因为一边太快导致内存爆掉。不管是文件、网络请求还是压缩数据,都是边读边处理,内存占用极低且全程非阻塞,是 Node.js 处理大数据的核心方案。
Node.js 有四种标准流类型,覆盖所有数据 I/O 场景:
可读流:用来读取数据,如文件读取流、HTTP 请求 req,负责产出数据。
可写流:用来写入数据,如文件写入流、HTTP 响应 res,负责消费数据。
双工流:同时可读可写,互不干扰,比如网络 Socket,用于双向通信。
转换流:属于特殊双工流,在读写过程中修改数据,如 zlib 压缩、加解密、数据解析。 四种流搭配使用,可完成读取、处理、写入、传输的完整链路。
流控制的核心是解决 ** 背压(Backpressure)** 问题,也就是写入速度远大于读取速度时的数据堆积。 当可写流缓冲区满了,会返回 false,告诉可读流先暂停推送;等缓冲区清空,会触发 drain 事件,恢复读取。 使用 pipe() 时,Node.js 会自动帮你做流控,不需要手动写 pause/resume。 手动控制时,可通过监听 drain、调用 pause() 和 resume() 实现。流控制保证内存不会暴涨,让大文件、高并发场景下的流稳定运行。
处理大文件绝对不能用 fs.readFile/writeFile,它们会把整个文件加载进内存,直接导致 OOM。 正确方案是使用文件流:fs.createReadStream 和 fs.createWriteStream。 通过 pipe() 把读流接到写流,自动分块读写、处理背压,内存只占用一小块缓冲区。 需要处理内容时,可在中间加转换流做解析、过滤、压缩。还能通过 highWaterMark 调整块大小。 这种方式内存占用恒定,GB 级文件也能轻松处理,是 Node.js 大文件标准方案。
Node.js 基于 V8 引擎做自动垃圾回收,用来释放不再使用的内存,避免泄漏。 V8 采用分代垃圾回收:把内存分为新生代(存活时间短)和老生代(存活时间长)。 新生代用复制算法,速度快;老生代用标记清除 + 标记整理。 回收时先从全局、执行栈等根对象出发,标记所有存活对象,然后清除未标记的垃圾。 V8 还做了增量标记、并发清理,减少主线程停顿时间。整个过程自动执行,开发者无需手动管理。
标记清除是 V8 主流 GC 算法:从根对象开始遍历,标记所有可达的存活对象,然后直接清除没被标记的对象。 优点是能解决循环引用问题,缺点是会产生内存碎片。
引用计数是给每个对象维护引用次数,被引用 +1,解除引用 -1,为 0 时立即回收。 优点是实时性高,不用暂停主线程;缺点是无法处理循环引用,会造成严重内存泄漏,且计数有额外开销。 Node.js 主用标记清除,几乎不用引用计数。
V8 默认对内存有严格限制:64 位系统下老生代约 1.4GB,32 位约 0.7GB,不能无限扩容。 启动时可通过 --max-old-space-size 手动调大上限。 处理大数据的核心思路:
不用数组全量存储,用流分块处理;
使用 Buffer,它占用堆外内存,不受 V8 限制;
分批、异步处理数据;
用 Redis、文件等外部存储做临时中转。 避开一次性加载,就能轻松突破内存限制,避免 OOM。
内存泄漏就是本该回收的对象,被意外长期引用,GC 无法回收,导致内存越占越多直到崩溃。 常见原因:全局变量泛滥、闭包过度持有引用、事件监听没移除、定时器未清理、缓存无限增长。
监控:用 process.memoryUsage() 实时看内存,或用 Chrome DevTools、heapdump 抓堆快照。 防止:避免意外全局变量;off 移除不用的事件;及时清定时器;缓存加过期策略;无用对象手动解除引用。 规范代码 + 定期监控,能避免绝大多数泄漏。
内存泄漏诊断一般分三步:
抓堆快照:用 Chrome DevTools 连接 Node.js,或 heapdump 生成快照;
定位异常对象:对比多次快照,找到持续增长的对象;
追溯引用链:看是谁在一直持有引用,定位到代码。
常用工具:Chrome DevTools、clinic.js、memwatch-next。 处理方式:移除多余事件监听、清理定时器 / 闭包引用、限制缓存大小、避免意外全局变量、解除无用对象引用。 修复后重新录制内存曲线,确认不再上涨即可。