Node 内存使用问题
一般后端开发语言中,在内存使用上没有什么限制。然而在 node 中使用的话会发现只能使用部分。
v8 在 64 位系统下只能使用 1.4GB 内存,在 32 位系统下只能使用 0.7GB 内存。复制代码
导致的问题:Node 无法直接操作大文件对象。
例如我想读取一个 4g 的文件来处理,即使物理内存有 32GB,在单个 Node 进程中也是不能完全的使用的。
内存限制的主要原因在于 Node 基于 v8 构建
v8 对象分配
所有的 JS 对象都是通过堆来进行分配的。使用 process.memoryUsage() 查看使用情况复制代码
> node> process.memoryUsage(){ rss: 21917696, // rss (resident set size) 进程的常驻内存 heapTotal: 7684096, // 已申请到的堆内存 heapUsed: 5147296, // 当前使用的堆内存 external: 8655}// 以上单位 字节复制代码
参考 Node.js v6.11.3 文档
heapTotal and heapUsed refer to V8's memory usage. external refers to the memory usage of C++ objects bound to JavaScript objects managed by V8.v8 垃圾回收机制
Node 内存限制主要原因是 v8 的垃圾回收制度。以 1.5GB 的垃圾回收堆内存为例,做一次小的回收需要 50MS,做一次非增量性回收需要 1S 以上,并且这会使 JS 线程暂停。因此限制内存。复制代码
除了堆外内存,其余都由 v8 管理
V8 的堆组成
V8 的堆由一系列区域组成:
新生代区:大多数对象的创建被分配在这里,这个区域很小,但垃圾回收非常频繁,独立于其它区。这里面的对象存活时间很短。
老生代指针区:包含大部分可能含有指向其它对象指针的对象。大多数从新生代晋升(存活一段时间)的对象会被移动到这里。
老生代数据区:包含原始数据对象(没有指针指向其它对象)。Strings、boxed numbers 以及双精度 unboxed 数组从新生代中晋升后被移到这里。
大对象区:这里存放大小超过其它区的大对象。每个对象都有自己 mmap 内存。大对象不会被回收。
代码区:代码对象(即包含被 JIT 处理后的指令对象)存放在此。唯一的有执行权限的区域(代码过大也可能存放在大对象区,此时它们也可被执行,但不代表大对象区都有执行权限)。
Cell 区、属性 Cell 区以及 map 区:包含 cell、属性 cell 以及 map。每个区都存放他们指向的相同大小以及相同结构的对象。
如何解除内存限制?
在启动 node 时,传递 --max-old-space-size=4096 (调整老生代内存限制,单位为 mb。--max-new-space-size 已经不可用了)
利用堆外内存: 使用 Buffer 类。Buffer 性能相关部分由 C++ 实现。Buffer 所占用的内存不是通过 v8 分配的,属于堆外内存。这样内存的分配回收的问题就丢给 c++ 来管了。
使用 stream 处理大文件
官方建议:it is recommended that you split your single process into several workers if you are hitting memory limits. (拆分进程)
垃圾回收机制
V8 的垃圾回收有如下几个特点
当处理一个垃圾回收周期时,暂停所有程序的执行。(stop-the-world 全停顿)
在大多数垃圾回收周期,每次仅处理部分堆中的对象,使暂停程序所带来的影响降至最低。(增量标记等算法)
-
准确知道在内存中所有的对象及指针,避免错误地把对象当成指针所带来的内存泄露。(标记指针法:在每个指针的末位预留一位来标记这个字代表的是指针或数据。)
-
在V8中,对象堆被分为两个部分:新创建的对象所在的新生代,以及在一个垃圾回收周期后存活的对象被提升到的老生代。
如果一个对象在一个垃圾回收周期中被移动,那么V8将会更新所有指向此对象的指针。
没有一种算法能够胜任所有场景,因此现代垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代实行不同算法
v8 将内存分为新生代和老生代
主要算法
新生代采用 Scavenge 算法 (打扫)
特点:牺牲空间换时间
将新生代堆内存一分为二,一个处于使用中 (from 空间),一个处于闲置(to 空间。
分配对象时先从 from 空间分配,垃圾回收时检查 from 空间中的存活对象,将存活对象复制到闲置空间中, 将非存活对象占用的空间释放。
完成复制后闲置空间和使用中空间角色互换。当一个对象经过多次复制依然存活时,它会被认为是生命周期较长的对象,会被晋升到老生代中。
晋升的条件有两个,一个是对象是否经历过 Scavenge 回收,一个是空闲空间的内存占用比超过 25%。
为什么是 25%
从使用空间中复制到空闲空间中,如果复制过来的已经占了空闲空间的一半,那么到时候交换成使用空间的时候,能用来分配的使用空间只剩 25%,这就有点少了,会有影响。
这种算法缺点:只能使用一半的堆空间,适合应用在新生代中。
Mark-Sweep & Mark-Compact (标记清除 & 标记整理)
Mark-Sweep 遍历堆中的所有对象,标记活着的对象。
在清除阶段清除所有没有被标记的对象。 缺点在于内存空间会出现不连续的状态。Mark-Compact 与 Mark-Sweep 的差别在于,在整理过程中,将活着的对象往一端移动,然后清理掉边界外的内存。
速度: Scavenge>Mark-Sweep>Mark-Compact
Incremental Marking (增量标记)
前三种算法都需要将应用逻辑暂停,待垃圾回收完成后再恢复,即 “全停顿”。
新生代较的打扫算法主要是复制存活对象,而这个数量是比较小的,所以全停顿影响不大。但老生代很大,标记清除整理过一遍造成全停顿影响很大。
为降低影响,将标记阶段改为增量标记,也就是将标记拆分为很多小标记,每做完一步就让 js 逻辑执行一会儿,垃圾回收与 JS 逻辑交替执行。
lazy sweeping & incremental compaction 等
内存泄漏
常见原因1.缓存2.队列消费不及时3.作用域未释放复制代码
case1 : 缓存
var cache = {};function set(key, value) { cache[key] = value;}复制代码
这样缓存对象会无限增大,也不能释放就会导致内存泄漏的问题。
可以对缓存的 key 值的个数加以限制。最好是使用进程外的缓存,如 memcahed 和 redis。
case2 : 无限增长的数组
var leakArr = [];function xx () { leakArr.push(Math.random());}复制代码
每一次调用 xx() , 导致 leakArr 不断的增加内存的占用。
如果非要这么设计,一定到增加清空队列相应的接口,以供调用者释放内存。
case3: 无限重连导致的内存泄漏
const net = require('net');let client = new net.Socket();function connect () { client.connect(26665, '127.0.0.1', function callbackListener() { console.log('connected!'); });}//第一次连接connect();client.on('error', function (error) { // console.error(error.message);});client.on('close', function () { //console.error('closed!'); //泄漏代码 client.destroy(); setTimeout(connect, 1);});复制代码
泄漏产生的原因其实也很简单:event.js 核心模块实现的事件发布 / 订阅本质上是一个 js 对象结构(在 v6 版本中为了性能采用了 new EventHandles(),并且把 EventHandles 的原型置为 null 来节省原型链查找的消耗),因此我们每一次调用 event.on 或者 event.once 相当于在这个对象结构中对应的 数组增加一个回调处理函数。
那么这个例子里面的泄漏属于非常隐蔽的一种:net 模块的重连每一次都会给 client 增加一个 connect 事件 的侦听器,如果一直重连不上,侦听器会无限增加,从而导致泄漏。
小测试,是否有内存泄漏
test1
var run = function () { var str = new Array(1000000).join('*'); var doSomethingWithStr = function () { if (str ==='something') console.log("str was something"); }; doSomethingWithStr();};setInterval(run, 1000);复制代码
test2
var run = function() { var str = new Array(1000000).join('*'); var logIt = function () { console.log('interval'); } setInterval(logIt, 100);};setInterval(run, 1000);复制代码
test3
var run = function() { var str = new Array(1000000).join('*'); var doSomethingWithStr = function () { if (str === 'something') console.log("str was something"); }; doSomethingWithStr(); var logIt = function () { console.log('interval'); } setInterval(logIt, 100);};setInterval(run, 1000);复制代码一旦一个变量被任一个闭包使用了,它会在所有的闭包词法环境结束之后才被释放,这会导致内存泄漏。
参考:
《深入浅出 Node.js》朴灵
suprising-javascript-memory-leak
v8 垃圾回收
典型的内存泄漏
- 三水清博客Node学习