加深对NodeJs内存管理的理解,更高效地使用内存来应对服务器端大量请求,避免长时间运行导致的内存泄漏。
相关文章
- 探索学习NodeJs内存管理
- NodeJs内存泄漏示例学习
- NodeJs内存泄漏分析工具
目录
- Node常驻内存模型
- v8堆内存分配
- v8垃圾回收
- v8内存限制
- 堆外内存
Node常驻内存模型
结合上图掌握node常驻内存几点:
常驻内存主要分为堆内存和堆外内存,堆内存由v8管理,堆外内存由使用对象对应模块的C++程序管理
-
查看常驻内存
console.log(process.memoryUsage())
输出:
{ rss: 21475328, heapTotal: 7159808, heapUsed: 4358568, external: 8224 }
rss
为常驻内存,是分配给该进程的物理内存
heapTotal
为v8管理的堆内存当前可以分配的大小
heapUsed
为v8管理的堆内存当前已使用的大小
external
为V8管理的,绑定到Javascript的C++对象的内存使用情况rss
包含v8管理的堆内存和堆外内存,会随着程序运行动态申请更多的内存 -
查看v8管理的堆内存
const v8 = require('v8') console.log(v8.getHeapSpaceStatistics())
输出:
[ { space_name: 'new_space', space_size: 2097152, space_used_size: 608784, space_available_size: 422384, physical_space_size: 2097152 }, { space_name: 'old_space', space_size: 2945024, space_used_size: 2564512, space_available_size: 97400, physical_space_size: 2945024 }, { space_name: 'code_space', space_size: 2097152, space_used_size: 1223168, space_available_size: 0, physical_space_size: 2097152 }, { space_name: 'map_space', space_size: 544768, space_used_size: 282744, space_available_size: 0, physical_space_size: 544768 }, { space_name: 'large_object_space', space_size: 0, space_used_size: 0, space_available_size: 1491770880, physical_space_size: 0 } ]
space_name与常驻内存模型中描述的对应,每个space简单介绍如下,引用于https://amsimple.com/blog/article/41.html
-
new_space
: 除去部分大对象(大于0.5MB),大部分的新对象都诞生在new_space -
old_space
: 大部分是从new_space中晋升过来的 -
map_space
: 所有在堆上分配的对象都带有指向它的隐藏类的指针,隐藏类保存在map_space隐藏类主要目的是为了优化对象访问速度,因为JS是动态类型语言,编译后,无法通过内存相对偏移快速访问属性,而借助隐藏类可以优化对象属性访问速度
-
code_space
: 代码对象,会分配在这,唯一拥有执行权限的内存 -
large_object_space
: 大于0.5MB的内存分配请求,会直接归类为large_object_space,垃圾回收时不会被移动或复制,提高效率
-
对常驻内存的介绍完毕,后文将结合示例探索v8的内存管理。
v8堆内存分配
通过在每次使用内存后打印堆内存的使用量,来分析堆内存是如何分配的。
示例一
const v8 = require('v8')
function format (bytes) {
return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}
function showMem (index) {
const heapStat = v8.getHeapSpaceStatistics()
console.log(
`loop ${index}: ` +
heapStat.map(
({ space_name, space_used_size }) => `${space_name}: ${format(space_used_size)}`
).join(',')
)
};
function useMem () {
return new Array(32 * 1024).fill(0)
};
let i = -1
const arr = []
while (++i < 10) {
showMem(i)
arr.push(useMem())
}
输出:
loop 0: new_space: 0.83 MB,old_space: 2.15 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 1: new_space: 0.74 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 2: new_space: 1.23 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 3: new_space: 1.73 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 4: new_space: 1.73 MB,old_space: 2.91 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 5: new_space: 0.74 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 6: new_space: 1.23 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 7: new_space: 1.73 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 8: new_space: 1.73 MB,old_space: 3.92 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 9: new_space: 2.22 MB,old_space: 3.92 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
从0-4次循环来看,new_space内存每次增加0.5M左右,而其他space内存基本上没发生变化;4-5次循环,new_space不变,old_space增加了0.5M;5-6次循环,new_space下降,old_space增加,new_space中部分对象转移到了old_space。
根据以上结果,应该能得出结论,新对象刚开始分配在new_space中,在一定情况下,new_space中的对象会转移到old_space中。
示例二
修改useMem方法,调整对象的大小为0.5M,如下:
function useMem () {
return new Array(64 * 1024).fill(0)
};
输出:
loop 0: new_space: 0.58 MB,old_space: 2.15 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 1: new_space: 0.58 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.50 MB
loop 2: new_space: 0.59 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1.00 MB
loop 3: new_space: 0.59 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1.50 MB
loop 4: new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 2.00 MB
loop 5: new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 2.50 MB
loop 6: new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 3.00 MB
loop 7: new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 3.50 MB
loop 8: new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 4.00 MB
loop 9: new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 4.50 MB
从结果上看,除了large_object_space,其他space基本没发生变化,说明对于大于0.5MB左右的对象直接分配到large_object_space。
v8垃圾回收
修改执行方法,让对象得以释放,观察垃圾回收日志
示例一
function useMem () {
new Array(32 * 1024).fill(0)
};
let i = -1
while (++i < 100000) {
showMem(i)
useMem()
}
将代码保存为文件test3.js,执行命令 node --trace_gc test3.js
,可以打印出每次垃圾回收日志。部分日志如下:
loop 7732: new_space: 3.69 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
[8716:000001CD96DFC300] 7669 ms: Scavenge 9.8 (17.8) -> 6.1 (17.8) MB, 0.3 / 0.0 ms allocation failure
loop 7733: new_space: 0.25 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7734: new_space: 0.74 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7735: new_space: 1.23 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7736: new_space: 1.73 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7737: new_space: 2.22 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7738: new_space: 2.71 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7739: new_space: 3.20 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7740: new_space: 3.69 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
[8716:000001CD96DFC300] 7676 ms: Scavenge 9.8 (17.8) -> 6.1 (17.8) MB, 0.2 / 0.0 ms allocation failure
loop 7741: new_space: 0.25 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
可以清楚地看到,new_space的内存到达某个值时,触发了垃圾回收,new_space中未引用的对象内存被清空。new_space中的GC是比较频繁的,里面大部分对象存活期较短,实际采用的算法为Scavenge。
Scavenge算法将new_space的总空间一分为二,只使用其中一个,另一个处于闲置,等待垃圾回收时使用。使用中的那块空间称为From,闲置的空间称为To。当新生代触发垃圾回收时,V8将From空间中所有应该存活下来的对象依次复制到To空间。
示例二
将对象的大小设置得大一些,如下:
function useMem () {
new Array(1024 * 1024).fill(0)
};
部分输出:
[72940:0000020B2D72B170] 2513 ms: Mark-sweep 108.1 (115.5) -> 28.0 (35.4) MB, 0.5 / 0.0 ms (+ 5.7 ms in 6 steps since start of marking, biggest step 3.7 ms, walltime since start of marking 47 ms) finalize incremental marking via stack guard GC in old space requested
与上一个例子中触发的GC不同,这里触发了增量标记算法的GC。在old_space中GC的算法主要为标记清除(Mark-Sweep)、标记整理(Mark-Compact)和增量标记(Incremental Marking)。
标记清除:当GC触发时,V8会将需要存活对象打上标记,然后将没有标记的对象,也就是需要死亡的对象,全部清除
标记整理:标记清除会导致可使用的内存不连续,因此在标记清除的基础上,标记整理将所有存活对象往一端移动,使内存空间紧挨
增量标记:v8在垃圾回收阶段,程序会被暂停,增量标记将标记阶段分为若干小步骤,每个步骤控制在5ms内,每运行一段时间标记动作,就让JavaScript程序执行一会儿,如此交替,一定程度上避免了长时间卡顿
小结
结合v8堆内存分配和v8垃圾回收的示例,初步感受了下v8的内存管理,关于内存管理的算法讲解,网上有大量详细的文章,感兴趣的可以通过文末的拓展学习参考中推荐的文章深入学习。
v8内存限制
示例一
function useMem () {
return new Array(20 * 1024 * 1024).fill(0)
};
let i = -1
const arr = []
while (++i < 10) {
showMem(i)
arr.push(useMem())
}
部分结果:
loop 8: new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1280.00 MB
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
在第9次循环时,提示堆内存溢出。
默认情况下,V8为堆分配的内存不超过1.4G:64位系统1.4G,32位则仅分配0.7G。新生代内存的最大值在64位系统和32位系统上分别为32MB和16 MB。可通过启动参数--max-old-space-size
设置老年代内存大小,可通过--max-semi-space-size
可设置新生代内存大小的一半。
堆内存为什么有限制?
内存太大,V8在GC时将要耗费更多的资源和时间,可能导致进程暂停时间过长。
示例二
将示例一保存为test4.js,执行命令node --max-old-space-size=2048 test4.js
最后部分结果:
loop 8: new_space: 0.69 MB,old_space: 2.23 MB,code_space: 1.17 MB,map_space: 0.28 MB,large_object_space: 1280.00 MB
loop 9: new_space: 0.69 MB,old_space: 2.23 MB,code_space: 1.17 MB,map_space: 0.28 MB,large_object_space: 1440.00 MB
可以看到最后占用的内存超过了之前的限制,程序正常执行。
堆外内存
使用Buffer对象,查看rss
变化
function format (bytes) {
return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}
function showMem (index) {
const { rss, heapTotal } = process.memoryUsage()
console.log(
`loop ${index}: ` +
`rss: ${format(rss)}, heapTotal: ${format(heapTotal)}`
)
};
function useMem () {
return Buffer.alloc(200 * 1024 * 1024).fill(0)
};
let i = -1
const arr = []
while (++i < 10) {
showMem(i)
arr.push(useMem())
}
结果:
loop 0: rss: 20.55 MB, heapTotal: 6.83 MB
loop 1: rss: 220.64 MB, heapTotal: 6.83 MB
loop 2: rss: 420.68 MB, heapTotal: 6.83 MB
loop 3: rss: 621.07 MB, heapTotal: 9.33 MB
loop 4: rss: 821.07 MB, heapTotal: 9.33 MB
loop 5: rss: 1021.07 MB, heapTotal: 9.33 MB
loop 6: rss: 1222.50 MB, heapTotal: 9.33 MB
loop 7: rss: 1422.50 MB, heapTotal: 9.33 MB
loop 8: rss: 1622.50 MB, heapTotal: 9.33 MB
loop 9: rss: 1822.51 MB, heapTotal: 9.33 MB
rss
超过了堆内存限制,且v8的堆内存基本没有发生改变,说明Buffer对象的内存在堆外内存中,不受v8内存限制。
当需要操作大文件时,受v8内存限制无法直接全部读取到内存中,如果不需要对文件进行操作,应该直接使用流读取,并且直接写入到其他管道中。如果需要对文件内容操作,应该先标准化文件内容,然后将文件读取到Buffer中,再一部分一部分地操作。
拓展学习参考
垃圾回收算法:
- 深入浅出Node.js#5.1.4
- https://juejin.im/post/5ad3f1156fb9a028b86e78be
- https://amsimple.com/blog/article/41.html
本文参考资源如下: