Node.js内存控制
Node.js内存控制
服务器端性能敏感,内存管理的好坏很关键。
V8的垃圾回收机制
V8的内存限制
单个Node进程只能使用部分内存。
64位系统约1.4G,32位系统约0.7G,这导致Node不能直接操作大内存对象。
原因:
V8引擎主要是服务于浏览器端的
垃圾回收机制的限制
V8的对象分配
查看内存使用量:
1 | process.memoryUsage(); |
返回值
1 | { |
堆内存是动态变化的,不足时会继续申请,直到超过V8限制。
V8的限制是可以放宽的,在Node启动时传递参数:
1 | // 单位是MB |
V8的垃圾回收
垃圾回收策略:分代式垃圾回收机制。
没有一种算法可以胜任所有场景,按对象的存活时间分代,匹配不同的回收算法。
内存分代:
新生代:存活时间短的对象,Scavenge回收算法
老生代:存活时间长
Scavenge算法
以空间换时间的算法
把堆内存分为两块,一个处于使用状态,一个处于闲置状态,
分配对象时,会放入使用空间中
当垃圾回收时,只把存活的对象复制到闲置区,清空活动区
此时,活跃区和闲置区互换
存在晋升的情况:即把对象移动到老生代内存中
Mark-Sweep & Mark-Compact算法
Mark-Sweep:
标记阶段:遍历堆中的对象,标记活的对象
清除阶段:清除没有被标记的对象
Mark-Sweep会导致内存空间会出现不连续状态。
Mark-Compact:基于Mark-Sweep
标记阶段:同上
整理阶段:标记后,把活的对象移动到内存的一端
清除阶段:清除边界外的内存
Mark-Compact需要移动对象,效率较低。
V8主要使用Mark-Sweep,内存不足时才使用Mark-Compact。
增量标记(Incremental Marking)
全停顿:垃圾回收时会暂停JS执行
全停顿时间过长后导致卡顿。
V8做了大量优化:
- 增量标记:把一次停顿拆分成多次执行。
查看垃圾回收日志
垃圾回收日志:启动参数--trace_gc
gc.log
性能分析日志:--prof
v8.log
Node提供了工具统计日志信息:tick-processor
高效使用内存
作用域
JS中能形成作用域的有函数、with和全局作用域。
标识符查找:沿着作用域链向上查找。
全局作用域直到进程退出才会释放,所以全局变量引用的对象会常驻在老生代内存中,需要主动释放:
delete
给全局变量重新赋值
闭包
作用域链上的对象访问只能向上,外部不能访问内部作用域。
实现外部访问内部变量的方法叫做“闭包”,高阶函数特性。
闭包的实现:函数的返回值是一个匿名函数,匿名函数可以访问函数的内部变量,外部函数通过这个匿名函数就可以访问到内部函数的变量。
闭包会导致匿名函数及其原始函数作用域得不到释放。
内存指标
查看内存使用情况
查看进程内存情况
1 | process.memoryUsage() |
rss:resident set size,驻留集合大小,进程的常驻内存部分
查看系统内存情况
1 | // 系统全部内存 |
堆外内存
进程的堆内存总是小于rss,这说明Node中的内存使用并非都是V8进行分配的。
不是通过V8分配的内存——称为“堆外内存”
V8的堆内存
Node的堆外内存
Buffer对象不经过V8的内存分配,也不会有堆内存的大小限制。这是因为,Node需要处理网络流和文件IO流,浏览器端一般不需要。
内存泄漏
内存泄漏的本质是:应当回收的对象出现意外没有被回收,常驻在老生代内存中。
造成内存泄漏的原因:
缓存
队列消费不及时
作用域未释放
缓存
慎将内存当做缓存,把一个对象当做缓存,会让该对象常驻老生代内存,如果不限定大小、没有过期策略会导致内存占用无限增长。
服务端程序是一个长时间运行程序,许多在浏览器、App等短时间运行程序中的可容忍的不良行为会持续放大,最终导致程序崩溃。
缓存限制策略
LRU算法
模块是常驻老生代内存的,设计时需要小心内存泄漏
进程外内存
使用专门的缓存软件实现缓存,如Radis、Memcached,好处:
减少常驻内存的对象数量
可以跨进程共享缓存
队列问题
队列在消费者-生产者模型中充当中间产物,一般情况下,消费速度远大于生产速度,不会出现内存泄漏,但一旦消费速度低于生产速度,就会形成堆积。
数据库的写入效率低于文件直接写入
解决方案:
采用消费速度更快的技术
监控队列长度报警
给调用提供超时机制和拒绝机制
内存泄漏排查
常见工具:
node-heapdump
node-mtrace
node-memwatch
大内存应用
存在操作大文件的场景
(1)通过流的方式读写大文件:stream模块
(2)如果不进行字符层面的操作,可使用纯粹的Buffer操作

