加深對NodeJs內(nèi)存管理的理解,更高效地使用內(nèi)存來應(yīng)對服務(wù)器端大量請求,避免長時間運行導(dǎo)致的內(nèi)存泄漏。
相關(guān)文章
- 探索學(xué)習(xí)NodeJs內(nèi)存管理
- NodeJs內(nèi)存泄漏示例學(xué)習(xí)
- NodeJs內(nèi)存泄漏分析工具
目錄
- Node常駐內(nèi)存模型
- v8堆內(nèi)存分配
- v8垃圾回收
- v8內(nèi)存限制
- 堆外內(nèi)存
Node常駐內(nèi)存模型

結(jié)合上圖掌握node常駐內(nèi)存幾點:
常駐內(nèi)存主要分為堆內(nèi)存和堆外內(nèi)存,堆內(nèi)存由v8管理,堆外內(nèi)存由使用對象對應(yīng)模塊的C++程序管理
-
查看常駐內(nèi)存
console.log(process.memoryUsage())輸出:
{ rss: 21475328, heapTotal: 7159808, heapUsed: 4358568, external: 8224 }rss為常駐內(nèi)存,是分配給該進程的物理內(nèi)存
heapTotal為v8管理的堆內(nèi)存當前可以分配的大小
heapUsed為v8管理的堆內(nèi)存當前已使用的大小
external為V8管理的,綁定到Javascript的C++對象的內(nèi)存使用情況rss包含v8管理的堆內(nèi)存和堆外內(nèi)存,會隨著程序運行動態(tài)申請更多的內(nèi)存 -
查看v8管理的堆內(nèi)存
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與常駐內(nèi)存模型中描述的對應(yīng),每個space簡單介紹如下,引用于https://amsimple.com/blog/article/41.html
-
new_space: 除去部分大對象(大于0.5MB),大部分的新對象都誕生在new_space -
old_space: 大部分是從new_space中晉升過來的 -
map_space: 所有在堆上分配的對象都帶有指向它的隱藏類的指針,隱藏類保存在map_space隱藏類主要目的是為了優(yōu)化對象訪問速度,因為JS是動態(tài)類型語言,編譯后,無法通過內(nèi)存相對偏移快速訪問屬性,而借助隱藏類可以優(yōu)化對象屬性訪問速度
-
code_space: 代碼對象,會分配在這,唯一擁有執(zhí)行權(quán)限的內(nèi)存 -
large_object_space: 大于0.5MB的內(nèi)存分配請求,會直接歸類為large_object_space,垃圾回收時不會被移動或復(fù)制,提高效率
-
對常駐內(nèi)存的介紹完畢,后文將結(jié)合示例探索v8的內(nèi)存管理。
v8堆內(nèi)存分配
通過在每次使用內(nèi)存后打印堆內(nèi)存的使用量,來分析堆內(nèi)存是如何分配的。
示例一
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次循環(huán)來看,new_space內(nèi)存每次增加0.5M左右,而其他space內(nèi)存基本上沒發(fā)生變化;4-5次循環(huán),new_space不變,old_space增加了0.5M;5-6次循環(huán),new_space下降,old_space增加,new_space中部分對象轉(zhuǎn)移到了old_space。
根據(jù)以上結(jié)果,應(yīng)該能得出結(jié)論,新對象剛開始分配在new_space中,在一定情況下,new_space中的對象會轉(zhuǎn)移到old_space中。
示例二
修改useMem方法,調(diào)整對象的大小為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
從結(jié)果上看,除了large_object_space,其他space基本沒發(fā)生變化,說明對于大于0.5MB左右的對象直接分配到large_object_space。
v8垃圾回收
修改執(zhí)行方法,讓對象得以釋放,觀察垃圾回收日志
示例一
function useMem () {
new Array(32 * 1024).fill(0)
};
let i = -1
while (++i < 100000) {
showMem(i)
useMem()
}
將代碼保存為文件test3.js,執(zhí)行命令 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的內(nèi)存到達某個值時,觸發(fā)了垃圾回收,new_space中未引用的對象內(nèi)存被清空。new_space中的GC是比較頻繁的,里面大部分對象存活期較短,實際采用的算法為Scavenge。
Scavenge算法將new_space的總空間一分為二,只使用其中一個,另一個處于閑置,等待垃圾回收時使用。使用中的那塊空間稱為From,閑置的空間稱為To。當新生代觸發(fā)垃圾回收時,V8將From空間中所有應(yīng)該存活下來的對象依次復(fù)制到To空間。
示例二
將對象的大小設(shè)置得大一些,如下:
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
與上一個例子中觸發(fā)的GC不同,這里觸發(fā)了增量標記算法的GC。在old_space中GC的算法主要為標記清除(Mark-Sweep)、標記整理(Mark-Compact)和增量標記(Incremental Marking)。
標記清除:當GC觸發(fā)時,V8會將需要存活對象打上標記,然后將沒有標記的對象,也就是需要死亡的對象,全部清除
標記整理:標記清除會導(dǎo)致可使用的內(nèi)存不連續(xù),因此在標記清除的基礎(chǔ)上,標記整理將所有存活對象往一端移動,使內(nèi)存空間緊挨
增量標記:v8在垃圾回收階段,程序會被暫停,增量標記將標記階段分為若干小步驟,每個步驟控制在5ms內(nèi),每運行一段時間標記動作,就讓JavaScript程序執(zhí)行一會兒,如此交替,一定程度上避免了長時間卡頓
小結(jié)
結(jié)合v8堆內(nèi)存分配和v8垃圾回收的示例,初步感受了下v8的內(nèi)存管理,關(guān)于內(nèi)存管理的算法講解,網(wǎng)上有大量詳細的文章,感興趣的可以通過文末的拓展學(xué)習(xí)參考中推薦的文章深入學(xué)習(xí)。
v8內(nèi)存限制
示例一
function useMem () {
return new Array(20 * 1024 * 1024).fill(0)
};
let i = -1
const arr = []
while (++i < 10) {
showMem(i)
arr.push(useMem())
}
部分結(jié)果:
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次循環(huán)時,提示堆內(nèi)存溢出。
默認情況下,V8為堆分配的內(nèi)存不超過1.4G:64位系統(tǒng)1.4G,32位則僅分配0.7G。新生代內(nèi)存的最大值在64位系統(tǒng)和32位系統(tǒng)上分別為32MB和16 MB??赏ㄟ^啟動參數(shù)--max-old-space-size設(shè)置老年代內(nèi)存大小,可通過--max-semi-space-size可設(shè)置新生代內(nèi)存大小的一半。
堆內(nèi)存為什么有限制?
內(nèi)存太大,V8在GC時將要耗費更多的資源和時間,可能導(dǎo)致進程暫停時間過長。
示例二
將示例一保存為test4.js,執(zhí)行命令node --max-old-space-size=2048 test4.js
最后部分結(jié)果:
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
可以看到最后占用的內(nèi)存超過了之前的限制,程序正常執(zhí)行。
堆外內(nèi)存
使用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())
}
結(jié)果:
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超過了堆內(nèi)存限制,且v8的堆內(nèi)存基本沒有發(fā)生改變,說明Buffer對象的內(nèi)存在堆外內(nèi)存中,不受v8內(nèi)存限制。
當需要操作大文件時,受v8內(nèi)存限制無法直接全部讀取到內(nèi)存中,如果不需要對文件進行操作,應(yīng)該直接使用流讀取,并且直接寫入到其他管道中。如果需要對文件內(nèi)容操作,應(yīng)該先標準化文件內(nèi)容,然后將文件讀取到Buffer中,再一部分一部分地操作。
拓展學(xué)習(xí)參考
垃圾回收算法:
- 深入淺出Node.js#5.1.4
- https://juejin.im/post/5ad3f1156fb9a028b86e78be
- https://amsimple.com/blog/article/41.html
本文參考資源如下: