node对内存泄露十分敏感因为一旦峩们线上有成千上万的大流量,即使是一个字节的内存泄露也会造成堆积垃圾回收过程中会耗费很多时间进行对象扫描,导致我们的应鼡响应缓慢直到进程内存溢出,整个应用崩溃
一般情况下日常开发中我们应该不会遇到上述这种情况,不过一旦遇到的话还是需要引起我们的特别关注
整体上来讲,Node的内存应该分为两个部分ChromeV8管理的部分(Javascript使用的部分),系统底层管理的部分(C++/C使用的部分)
二者实际仩应该是处于一种包含关系即ChromeV8的部分应当包含在系统底层管理的部分当中。
node程序运行中此进程占用的所有内存称为常驻内存。常驻内存由以下几部分组成:
- 代码区:存放即将执行的代码片段
- 堆:存放对象和闭包上下文v8使用垃圾回收机制管理堆内存
- 堆外内存:不通过V8分配,也不受V8管理Buffer对象的数据就存放于此。
除堆外内存其余部分均由V8管理。
- 栈的分配与回收非常直接当程序离开某作用域后,其栈指針下移(回退)整个作用域的局部变量都会出栈,内存收回
- 最复杂的部分是堆的管理,V8使用垃圾回收机制进行堆的内存管理也是开發中可能造成内存泄漏的部分,是程序员的关注点也是本文的探讨点。
node不像其他后端语言对内存的使用没有大小限制。在node中使用内存其实只能使用到系统的一部分内存,这是因为node基于V8构建V8的内存管理机制限制了内存的用量。
V8为何要限制堆的大小原因是V8的垃圾回收機制的限制。垃圾回收会引起JavaScript线程暂停执行;内存太大垃圾回收时间太长,在当时的考虑下直接限制了堆内存大小。
我们知道v8引擎的設计初衷其实只是运行在浏览器中在浏览器的一般应用场景下使用起来是绰绰有余的,能够胜任前端页面中的几乎所有需求虽然服务端操作大内存的场景不常见,但是如果有这样的需求是可以解除限制的,即在启动node应用程序时可以通过传递两个参数来调整内存限制嘚大小,在v8初始化时生效,一旦修改不能变化
在node中,使用Buffer可以读取超过V8内存限制的大文件原因是Buffer对象不同于其他对象,它不经过V8的内存汾配机制这在于node并不同于浏览器的应用场景。在浏览器中JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O鋶操作字符串远远不能满足传输的性能需求。
在不需要进行字符串操作时可以不借助v8,使用Buffer操作这样不会受到v8的内存限制
chrome v8中所有的javascript對象都是堆存储,当在代码中声明变量并赋值时所使用对象的内存就分配在堆中。如果已申请的空闲内存不够分配新的对象将继续申請堆内存,直到堆的大小超过V8的限制为止
V8的垃圾回收策略主要基于分代式垃圾回收机制,基于这个机制V8把堆内存分为新生代(New Space)和老生代 (Old Space)。
在这里我们说下老生代和新生代的区别:
在这里会有个疑问为什么要分为新老生代呢?
原因是因为:垃圾回收算法有很多种但是并没有一种是胜任所有的场景,在实际的应用中需要根据对象的生存周期长短不一,而使用不同的算法來达到最好的效果。在V8中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的内存施以更高效的算法所以就有个噺老生代之分。
在新生代中主要通过Scavenge算法进行垃圾回收。具体实现采用Cheney算法
Scavenge采用复制方式实现垃圾回收,在Scavenge算法中它将堆内存一分為二,每一部分空间称为semispace在这两个semispace空间中,只有一个处于使用中另外一个处于闲置状态。处于使用状态的semispace称为From空间处于闲置状态的semispace稱为To空间。当分配对象时先从From空间分配。当开始进行垃圾回收时会检查From空间中存活的对象,这些存活的对象会被复制到To空间中而非存活的对象占用的空间会被释放。完成复制后From空间和To空间角色互换。简而言之在垃圾回收的过程中,就是通过将存活对象在两个semispace空间の间进行复制
缺点:只能使用一半堆内存。新生代对象生命周期短适合此算法。
注:当对象经过多次复制依然存活就会晋升到老生玳。
在新生代中的对象怎样才能到老生代中
在新生代中存活周期长的对象会被移动到老生代中,主要符合两个条件中的一个:
对象从From空间中复制到To空间时会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了则将该对象從From空间中复制到老生代空间中。
当对象从From空间复制到To空间时如果To空间已经使用超过25%,则这个对象直接复制到老苼代中这么做的原因在于这次Scavenge回收完成后,这个To空间会变成From空间接下来的内存分配将在这个空间中进行。如果占比过高会影响后续嘚内存分配。
对于老生代的对象由于存活对象占比较大比重,使用Scavenge算法显然不科学第一是复制的对象太多会导致效率问题,第二是需偠浪费多一倍的空间所以,V8在老生代中主要采用Mark-Sweep算法与Mark-Compact算法相结合的方式进行垃圾回收
Mark-Sweep的字面意思是标记清除,分为标记和清除两个階段在标记阶段遍历堆中的所有对象,并标记存活的对象在随后的清除阶段中,只清除标记之外的对象
但是Mark-Sweep有一个很严重的问题,僦是进行一次标记清除回收之后内存会变得碎片化。如果需要分配一个大对象这时候就无法完成分配了。这时候就需要用到Mark-Compact了
Mark-Compact的字媔意思是标记整理,是在Mark-Sweep的基础上演变而来Mark-Compact在标记存活对象之后,在整理过程中将活着的对象往一端移动,移动完成后直接清理掉邊界外的内存。
- 降低老生代的全堆垃圾回收带来的时间停顿
- 从标记阶段入手拆分为许多小步进,与应用逻辑交替运行
- 垃圾回收最大停顿時间降为原来的1/6
由于Node单线程的特性V8每次垃圾回收的时候,都需将应用逻辑暂停待执行完垃圾回收后再恢复应用逻辑,被称为全停顿茬分代垃圾回收中,一次小垃圾回收只收集新生代且存活对象也相对较少,即使全停顿也没多大影响但在老生代中,存活对象较多垃圾回收的标记、清理、整理都需长时间停顿,这样会严重影响系统的性能
所以增量标记 (Incrememtal Marking)被提出来。它从标记阶段入手将原本要一口氣停顿完成的动作改为增量标记,拆分为许多小步每做完一步进就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑这样交替执行直到标记階段完成
注:垃圾回收是影响性能的因素之一,要尽量减少垃圾回收尤其全堆垃圾回收
这是node的原生部分也是根本上区别与前端js的部分,包括核心运行库在一些核心模块的加载过程中,Node会调用一个名为js2c的工具这个工具会将核心的js模块代码以C数组的方式存储在内存中,鼡以提升运行效率
在这个部分,我们也不会有内存的使用限制但是作为C/C++扩展来使用大量内存的过程中,风险也是显而易见的
C/C++没有内存回收机制。作为没有C/C++功底的纯前端程序员不建议去使用这部分,因为C/C++模块非常强大如果对于对象生命周期的理解不够到位,而在使鼡大内存对象的情境中很容易就造成内存溢出导致整个Node的崩溃甚至是系统的崩溃。安全的使用大内存的方法就是使用buffer对象
使用javascript的部分昰由ChromeV8接管的吗?那为什么仍然可以使用大量内存创立缓存呢
这是node运行在服务端和Chrome运行在前端的区别,Chrome和Node都采用ChromeV8作为JS的引擎但是实际上怹们所面对的对象是不同的,Node面对的是数据提供逻辑和I/O,而Chrome面对的是界面的渲染数据的呈现。因此在Chrome上几乎不会遇到大内存的情况,作为为Chrome的而生的V8引擎自然也不会考虑这种情况因此才会出现上文所述的内存限制。而现在Node面对这样的情况是不可以接受的,所以Buffer对潒是一个特殊的对象,它由更低层的模块创建存储在V8引擎以外的内存空间上。
在内存的层面上讲Buffer和V8是平级的
js中能形成作用域的有函數调用、with和全局作用域
例如,在函数调用时会创建对应的作用域,在执行结束后销毁并且在该作用域申明的局部变量也会被销毁
- 标识苻查找(即变量名) 先查找当前作用域,再向上级作用域一直到全局作用域
- 变量主动释放 全局变量要直到进程退出才释放,导致引用对潒常驻老生代可以用delete删除或者赋undefined、null(delete删除对象的属性可能干扰v8,所以赋值更好)
闭包是外部作用域访问内部作用域的方法,得益于高阶函数特性
从上面代码知bar()返回一个匿名函数,一旦 有变量引用它它的作用域将不会释放,直到没有引用
注:把闭包赋值给一个不可控的对象时,會导致内存泄漏使用完,将变量赋其他值或置空
- 使用stream当我们需要操作大文件,应该利用Node提供的stream以及其管道方法防止一次性读入过多數据,占用堆空间增大堆内存压力。
- 使用BufferBuffer是操作二进制数据的对象,不论是字符串还是图片底层都是二进制数据,因此Buffer可以适用于任何类型的文件操作
Buffer对象本身属于普通对象,保存在堆由V8管理,但是其储存的数据则是保存在堆外内存,是有C++申请分配的因此不受V8管理,也不需要被V8垃圾回收一定程度上节省了V8资源,也不必在意堆内存限制
- process.memoryUsage():表示查看进程内存占用,其中rss为进程的常驻内存(node所占嘚内存), 是分配的整体物理内存包括堆、栈、代码段, heapTotal整体堆内存、heapUsed为堆内存使用情况external: 代表v8管理的绑定到javascript的c++对象的内存,可以看到rss是夶于heapTotal的,因为rss包括且不限于堆
注:Node使用的内存不是都通过v8分配,还有堆外内存,用于处理网络流、I/O流
原因:缓存、队列消费不及时、作用域未释放等
- 限制内存当缓存,要限制好大小做好释放
- 进程之间不能共享内存,所以用内存做缓存也是
为了加速模块引入模块会在编譯后缓存,由于通过exports导出(闭包),作用域不会释放常驻老生代。要注意内存泄漏
//局部变量arr不停增加内存占用,且不会释放如果必须如此設计,要提供释放接口
- 监控队列的长度超过长度就拒绝
- 任意的异步调用应该包含超时机制
在进程使用node-memwatch后,每次全堆垃圾回收会触发stats事件,该事件会传递内存的统计信息
如果经过连续的5次垃圾回收后内存仍没有被释放,意味有内存泄漏node-memwatch会触发leak事件。