本篇文章帶大家深入了解NodeJS V8引擎的內(nèi)存和垃圾回收器(GC),希望對(duì)大家有所幫助!
一、為什么需要GC
程序應(yīng)用運(yùn)行需要使用內(nèi)存,其中內(nèi)存的兩個(gè)分區(qū)是我們常常會(huì)討論的概念:棧區(qū)和堆區(qū)。
棧區(qū)是線性的隊(duì)列,隨著函數(shù)運(yùn)行結(jié)束自動(dòng)釋放的,而堆區(qū)是自由的動(dòng)態(tài)內(nèi)存空間、堆內(nèi)存是手動(dòng)分配釋放或者 垃圾回收程序(Garbage Collection,后文都簡(jiǎn)稱GC)自動(dòng)分配釋放的。
軟件發(fā)展早期或者一些語(yǔ)言對(duì)于堆內(nèi)存都是手動(dòng)操作分配和釋放,比如 C、C++。雖然能精準(zhǔn)操作內(nèi)存,達(dá)到盡可能的最優(yōu)內(nèi)存使用,但是開發(fā)效率卻非常低,也容易出現(xiàn)內(nèi)存操作不當(dāng)。【相關(guān)教程推薦:nodejs視頻教程、編程教學(xué)】
隨著技術(shù)發(fā)展,高級(jí)語(yǔ)言(例如Java Node)都不需要開發(fā)者手動(dòng)操作內(nèi)存,程序語(yǔ)言自動(dòng)會(huì)分配和釋放空間。同時(shí)也誕生了 GC(Garbage Collection)垃圾回收器,幫助釋放和整理內(nèi)存。開發(fā)者大部分情況不需要關(guān)心內(nèi)存本身,可以專注業(yè)務(wù)開發(fā)。后文主要是討論堆內(nèi)存和 GC。
二、GC發(fā)展
GC運(yùn)行會(huì)消耗CPU資源,GC運(yùn)行的過(guò)程會(huì)觸發(fā)STW(stop-the-world)暫停業(yè)務(wù)代碼線程,為什么會(huì) STW 呢?是為了保證在 GC 的過(guò)程中,不會(huì)和新創(chuàng)建的對(duì)象起沖突。
GC主要是伴隨內(nèi)存大小增加而發(fā)展演化。大致分為3個(gè)大的代表性階段:
- 階段一 單線程GC(代表:serial)
單線程GC,在它進(jìn)行垃圾收集時(shí),必須完全暫停其他所有的工作線程 ,它是最初階段的GC,性能也是最差的
- 階段二 并行多線程GC(代表:Parallel Scavenge, ParNew)
在多 CPU 環(huán)境中利用多條 GC 線程同時(shí)并行運(yùn)行,從而垃圾回收的時(shí)間減少、用戶線程停頓的時(shí)間也減少,這個(gè)算法也會(huì)STW,完全暫停其他所有的工作線程
- 階段三 多線程并發(fā) concurrent GC(代表:CMS (Concurrent Mark Sweep) G1)
這里的并發(fā)是指:GC多線程執(zhí)行可以和業(yè)務(wù)代碼并發(fā)運(yùn)行。
在前面的兩個(gè)發(fā)展階段的 GC 算法都會(huì)完全 STW,而在 concurrent GC 中,有部分階段 GC 線程可以和業(yè)務(wù)代碼并發(fā)運(yùn)行,保證了更短的 STW 時(shí)間。但是這個(gè)模式就會(huì)存在標(biāo)記錯(cuò)誤,因?yàn)?GC 過(guò)程中可能有新對(duì)象進(jìn)來(lái),當(dāng)然算法本身會(huì)修正和解決這個(gè)問(wèn)題
上面的三個(gè)階段并不代表 GC 一定是上面描述三種的其中一種。不同程序語(yǔ)言的 GC 根據(jù)不同需求采用多種算法組合實(shí)現(xiàn)。
三、v8 內(nèi)存分區(qū)與GC
堆內(nèi)存設(shè)計(jì)與GC設(shè)計(jì)是緊密相關(guān)的。V8 把堆內(nèi)存分為幾大區(qū)域,采用分代策略。
盜圖:
- 新生代(new-space 或 young-generation):空間小,分為了兩個(gè)半空間(semi-space),其中的數(shù)據(jù)存活期短。
- 老生代(old-space 或 old-generation):空間大,可增量,其中的數(shù)據(jù)存活期長(zhǎng)
- 大對(duì)象空間(large-object-space):默認(rèn)超過(guò)256K的對(duì)象會(huì)在此空間下,下文解釋
- 代碼空間(code-space):即時(shí)編譯器(JIT)在這里存儲(chǔ)已編譯的代碼
- 元空間 (cell space):這個(gè)空間用于存儲(chǔ)小的、固定大小的JavaScript對(duì)象,比如數(shù)字和布爾值。
- 屬性元空間 (property cell space):這個(gè)空間用于存儲(chǔ)特殊的JavaScript對(duì)象,比如訪問(wèn)器屬性和某些內(nèi)部對(duì)象。
- Map Space:這個(gè)空間用于存儲(chǔ)用于JavaScript對(duì)象的元信息和其他內(nèi)部數(shù)據(jù)結(jié)構(gòu),比如Map和Set對(duì)象。
3.1 分代策略:新生代和老生代
在 Node.js 中,GC 采用分代策略,分為新、老生代區(qū),內(nèi)存數(shù)據(jù)大都在這兩個(gè)區(qū)域。
3.1.1 新生代
新生代是一個(gè)小的、存儲(chǔ)年齡小的對(duì)象、快速的內(nèi)存池,分為了兩個(gè)半空間(semi-space),一半的空間是空閑的(稱為to空間),另一半的空間是存儲(chǔ)了數(shù)據(jù)(稱為from空間)。
當(dāng)對(duì)象首次創(chuàng)建時(shí),它們被分配到新生代 from 半空間中,它的年齡為1。當(dāng) from 空間不足或者超過(guò)一定大小數(shù)量之后,會(huì)觸發(fā) Minor GC(采用復(fù)制算法 Scavenge),此時(shí),GC 會(huì)暫停應(yīng)用程序的執(zhí)行(STW,stop-the-world),標(biāo)記(from空間)中所有活動(dòng)對(duì)象,然后將它們整理連續(xù)移動(dòng)到新生代的另一個(gè)空閑空間(to空間)中。最后原本的 from 空間的內(nèi)存會(huì)被全部釋放而變成空閑空間,兩個(gè)空間就完成 from 和 to 的對(duì)換,復(fù)制算法是犧牲了空間換取時(shí)間的算法。
新生代的空間更小,所以此空間會(huì)更頻繁的觸發(fā) GC。同時(shí)掃描的空間更小,GC性能消耗也更小、它的 GC 執(zhí)行時(shí)間也更短。
每當(dāng)一次 Minor GC 完成存活的對(duì)象年齡就+1,經(jīng)歷過(guò)多次Minor GC還存活的對(duì)象(年齡大于N),它們將被移動(dòng)到老生代內(nèi)存池中。
3.1.2 老生代
老生代是一個(gè)大的內(nèi)存池,用于存儲(chǔ)較長(zhǎng)壽命的對(duì)象。老生代內(nèi)存采用 標(biāo)記清除(Mark-Sweep)、標(biāo)記壓縮算法(Mark-Compact)。它的一次執(zhí)行叫做 Mayor GC。當(dāng)老生代中的對(duì)象占滿一定比例時(shí),即存活對(duì)象與總對(duì)象的比例超過(guò)一定的閾值,就會(huì)觸發(fā)一次 標(biāo)記清除 或 標(biāo)記壓縮。
因?yàn)樗目臻g更大,它的GC執(zhí)行時(shí)間也更長(zhǎng),頻率相對(duì)新生代更低。如果老生代完成 GC 回收之后空間還是不足,V8 就會(huì)從系統(tǒng)中申請(qǐng)更多內(nèi)存。
可以手動(dòng)執(zhí)行 global.gc() 方法,設(shè)置不同參數(shù),主動(dòng)觸發(fā)GC。 但是需要注意的是,默認(rèn)情況下,Node.js 是禁用了此方法。如果要啟用,可以通過(guò)啟動(dòng) Node.js 應(yīng)用程序時(shí)添加 --expose-gc 參數(shù)來(lái)開啟,例如:
node --expose-gc app.js
V8 在老生代中主要采用了 Mark-Sweep 和 Mark-Compact 相結(jié)合的方式進(jìn)行垃圾回收。
Mark-Sweep 是標(biāo)記清除的意思,它分為兩個(gè)階段,標(biāo)記和清除。Mark-Sweep 在標(biāo)記階段遍歷堆中的所有對(duì)象,并標(biāo)記活著的對(duì)象,在隨后的清除階段中,只清除未被標(biāo)記的對(duì)象。
Mark-Sweep 最大的問(wèn)題是在進(jìn)行一次標(biāo)記清除回收后,內(nèi)存空間會(huì)出現(xiàn)不連續(xù)的狀態(tài)。這種內(nèi)存碎片會(huì)對(duì)后續(xù)的內(nèi)存分配造成問(wèn)題,因?yàn)楹芸赡艹霈F(xiàn)需要分配一個(gè)大對(duì)象的情況,這時(shí)所有的碎片空間都無(wú)法完成此次分配,就會(huì)提前觸發(fā)垃圾回收,而這次回收是不必要的。
為了解決 Mark-Sweep 的內(nèi)存碎片問(wèn)題,Mark-Compact 被提出來(lái)。Mark-Compact 是標(biāo)記整理的意思,是在 Mark-Sweep 的基礎(chǔ)上演進(jìn)而來(lái)的。它們的差別在于對(duì)象在標(biāo)記為死亡后,在整理過(guò)程中,將活著的對(duì)象往一端移動(dòng),移動(dòng)完成后,直接清理掉邊界外的內(nèi)存。V8 也會(huì)根據(jù)一定邏輯,釋放一定空閑的內(nèi)存還給系統(tǒng)。
3.2 大對(duì)象空間 large object space
大對(duì)象會(huì)直接在大對(duì)象空間創(chuàng)建,并且不會(huì)移動(dòng)到其它空間。那么到底多大的對(duì)象會(huì)直接在大對(duì)象空間創(chuàng)建,而不是在新生代 from 區(qū)中創(chuàng)建呢?查閱資料和源代碼終于找到了答案。默認(rèn)情況下是 256K,V8 似乎并沒(méi)有暴露修改命令,源碼中的 v8_enable_hugepage 配置應(yīng)該是打包的時(shí)候設(shè)定的。
// There is a separate large object space for objects larger than // Page::kMaxRegularHeapObjectSize, so that they do not have to move during // collection. The large object space is paged. Pages in large object space // may be larger than the page size.
(1 << (18 - 1)) 的結(jié)果 256K (1 << (19 - 1)) 的結(jié)果 256K (1 << (21 - 1)) 的結(jié)果 1M(如果開啟了hugPage)
四、V8 新老分區(qū)大小
4.1 老生代分區(qū)大小
在v12.x 之前:
為了保證 GC 的執(zhí)行時(shí)間保持在一定范圍內(nèi),V8 限制了最大內(nèi)存空間,設(shè)置了一個(gè)默認(rèn)老生代內(nèi)存最大值,64位系統(tǒng)中為大約1.4G,32位為大約700M,超出會(huì)導(dǎo)致應(yīng)用崩潰。
如果想加大內(nèi)存,可以使用 --max-old-space-size 設(shè)置最大內(nèi)存(單位:MB)
node --max_old_space_size=
在v12以后:
V8 將根據(jù)可用內(nèi)存分配老生代大小,也可以說(shuō)是堆內(nèi)存大小,所以并沒(méi)有限制堆內(nèi)存大小。以前的限制邏輯,其實(shí)不合理,限制了 V8 的能力,總不能因?yàn)?GC 過(guò)程消耗的時(shí)間更長(zhǎng),就不讓我繼續(xù)運(yùn)行程序吧,后續(xù)的版本也對(duì) GC 做了更多優(yōu)化,內(nèi)存越來(lái)越大也是發(fā)展需要。
如果想要做限制,依然可以使用 --max-old-space-size 配置, v12 以后它的默認(rèn)值是0,代表不限制。
參考文檔:nodejs.medium.com/introducing…
4.2 新生代分區(qū)大小
新生代中的一個(gè) semi-space 大小 64位系統(tǒng)的默認(rèn)值是16M,32位系統(tǒng)是8M,因?yàn)橛?個(gè) semi-space,所以總大小是32M、16M。
--max-semi-space-size
--max-semi-space-size 設(shè)置新生代 semi-space 最大值,單位為MB。
此空間不是越大越好,空間越大掃描的時(shí)間就越長(zhǎng)。這個(gè)分區(qū)大部分情況下是不需要做修改的,除非針對(duì)具體的業(yè)務(wù)場(chǎng)景做優(yōu)化,謹(jǐn)慎使用。
--max-new-space-size
--max-new-space-size 設(shè)置新生代空間最大值,單位為KB(不存在)
有很多文章說(shuō)到此功能,我翻了下 nodejs.org 網(wǎng)頁(yè)中 v4 v6 v7 v8 v10的文檔都沒(méi)有看到有這個(gè)配置,使用 node --v8-options 也沒(méi)有查到,也許以前的某些老版本有,而現(xiàn)在都應(yīng)該使用 --max-semi-space-size。
五、 內(nèi)存分析相關(guān)API
5.1 v8.getHeapStatistics()
執(zhí)行 v8.getHeapStatistics(),查看 v8 堆內(nèi)存信息,查詢最大堆內(nèi)存 heap_size_limit,當(dāng)然這里包含了新、老生代、大對(duì)象空間等。我的電腦硬件內(nèi)存是 8G,Node版本16x,查看到 heap_size_limit 是4G。
{ total_heap_size: 6799360, total_heap_size_executable: 524288, total_physical_size: 5523584, total_available_size: 4340165392, used_heap_size: 4877928, heap_size_limit: 4345298944, malloced_memory: 254120, peak_malloced_memory: 585824, does_zap_garbage: 0, number_of_native_contexts: 2, number_of_detached_contexts: 0 }
到 k8s 容器中查詢 NodeJs 應(yīng)用,分別查看了v12 v14 v16版本,如下表??雌饋?lái)是本身系統(tǒng)當(dāng)前的最大內(nèi)存的一半。128M 的時(shí)候,為啥是 256M,因?yàn)槿萜髦羞€有交換內(nèi)存,容器內(nèi)存實(shí)際最大內(nèi)存限制是內(nèi)存限制值 x2,有同等的交換內(nèi)存。
所以結(jié)論是大部分情況下 heap_size_limit 的默認(rèn)值是系統(tǒng)內(nèi)存的一半。但是如果超過(guò)這個(gè)值且系統(tǒng)空間足夠,V8 還是會(huì)申請(qǐng)更多空間。當(dāng)然這個(gè)結(jié)論也不是一個(gè)最準(zhǔn)確的結(jié)論。而且隨著內(nèi)存使用的增多,如果系統(tǒng)內(nèi)存還足夠,這里的最大內(nèi)存還會(huì)增長(zhǎng)。
容器最大內(nèi)存 | heap_size_limit |
---|---|
4G | 2G |
2G | 1G |
1G | 0.5G |
1.5G | 0.7G |
256M | 256M |
128M | 256M |
5.2 process.memoryUsage
process.memoryUsage() { rss: 35438592, heapTotal: 6799360, heapUsed: 4892976, external: 939130, arrayBuffers: 11170 }
通過(guò)它可以查看當(dāng)前進(jìn)程的內(nèi)存占用和使用情況 heapTotal、heapUsed,可以定時(shí)獲取此接口,然后繪畫出折線圖幫助分析內(nèi)存占用情況。以下是 Easy-Monitor 提供的功能:
建議本地開發(fā)環(huán)境使用,開啟后,嘗試大量請(qǐng)求,會(huì)看到內(nèi)存曲線增長(zhǎng),到請(qǐng)求結(jié)束之后,GC觸發(fā)后會(huì)看到內(nèi)存曲線下降,然后再嘗試多次發(fā)送大量請(qǐng)求,這樣往復(fù)下來(lái),如果發(fā)現(xiàn)內(nèi)存一直在增長(zhǎng)低谷值越來(lái)越高,就可能是發(fā)生了內(nèi)存泄漏。
5.3 開啟打印GC事件
使用方法
node --trace_gc app.js // 或者 v8.setFlagsFromString('--trace_gc');
- --trace_gc
[40807:0x148008000] 235490 ms: Scavenge 247.5 (259.5) -> 244.7 (260.0) MB, 0.8 / 0.0 ms (average mu = 0.971, current mu = 0.908) task [40807:0x148008000] 235521 ms: Scavenge 248.2 (260.0) -> 245.2 (268.0) MB, 1.2 / 0.0 ms (average mu = 0.971, current mu = 0.908) allocation failure [40807:0x148008000] 235616 ms: Scavenge 251.5 (268.0) -> 245.9 (268.8) MB, 1.9 / 0.0 ms (average mu = 0.971, current mu = 0.908) task [40807:0x148008000] 235681 ms: Mark-sweep 249.7 (268.8) -> 232.4 (268.0) MB, 7.1 / 0.0 ms (+ 46.7 ms in 170 steps since start of marking, biggest step 4.2 ms, walltime since start of marking 159 ms) (average mu = 1.000, current mu = 1.000) finalize incremental marking via task GC in old space requested
GCType <heapUsed before> (<heapTotal before>) -> <heapUsed after> (<heapTotal after>) MB
上面的 Scavenge 和 Mark-sweep 代表GC類型,Scavenge 是新生代中的清除事件,Mark-sweep 是老生代中的標(biāo)記清除事件。箭頭符號(hào)前是事件發(fā)生前的實(shí)際使用內(nèi)存大小,箭頭符號(hào)后是事件結(jié)束后的實(shí)際使用內(nèi)存大小,括號(hào)內(nèi)是內(nèi)存空間總值??梢钥吹叫律惺录l(fā)生的頻率很高,而后觸發(fā)的老生代事件會(huì)釋放總內(nèi)存空間。
- --trace_gc_verbose
展示堆空間的詳細(xì)情況
v8.setFlagsFromString('--trace_gc_verbose'); [44729:0x130008000] Fast promotion mode: false survival rate: 19% [44729:0x130008000] 97120 ms: [HeapController] factor 1.1 based on mu=0.970, speed_ratio=1000 (gc=433889, mutator=434) [44729:0x130008000] 97120 ms: [HeapController] Limit: old size: 296701 KB, new limit: 342482 KB (1.1) [44729:0x130008000] 97120 ms: [GlobalMemoryController] Limit: old size: 296701 KB, new limit: 342482 KB (1.1) [44729:0x130008000] 97120 ms: Scavenge 302.3 (329.9) -> 290.2 (330.4) MB, 8.4 / 0.0 ms (average mu = 0.998, current mu = 0.999) task [44729:0x130008000] Memory allocator, used: 338288 KB, available: 3905168 KB [44729:0x130008000] Read-only space, used: 166 KB, available: 0 KB, committed: 176 KB [44729:0x130008000] New space, used: 444 KB, available: 15666 KB, committed: 32768 KB [44729:0x130008000] New large object space, used: 0 KB, available: 16110 KB, committed: 0 KB [44729:0x130008000] Old space, used: 253556 KB, available: 1129 KB, committed: 259232 KB [44729:0x130008000] Code space, used: 10376 KB, available: 119 KB, committed: 12944 KB [44729:0x130008000] Map space, used: 2780 KB, available: 0 KB, committed: 2832 KB [44729:0x130008000] Large object space, used: 29987 KB, available: 0 KB, committed: 30336 KB [44729:0x130008000] Code large object space, used: 0 KB, available: 0 KB, committed: 0 KB [44729:0x130008000] All spaces, used: 297312 KB, available: 3938193 KB, committed: 338288 KB [44729:0x130008000] Unmapper buffering 0 chunks of committed: 0 KB [44729:0x130008000] External memory reported: 20440 KB [44729:0x130008000] Backing store memory: 22084 KB [44729:0x130008000] External memory global 0 KB [44729:0x130008000] Total time spent in GC : 199.1 ms
- --trace_gc_nvp
每次GC事件的詳細(xì)信息,GC類型,各種時(shí)間消耗,內(nèi)存變化等
v8.setFlagsFromString('--trace_gc_nvp'); [45469:0x150008000] 8918123 ms: pause=0.4 mutator=83.3 gc=s reduce_memory=0 time_to_safepoint=0.00 heap.prologue=0.00 heap.epilogue=0.00 heap.epilogue.reduce_new_space=0.00 heap.external.prologue=0.00 heap.external.epilogue=0.00 heap.external_weak_global_handles=0.00 fast_promote=0.00 complete.sweep_array_buffers=0.00 scavenge=0.38 scavenge.free_remembered_set=0.00 scavenge.roots=0.00 scavenge.weak=0.00 scavenge.weak_global_handles.identify=0.00 scavenge.weak_global_handles.process=0.00 scavenge.parallel=0.08 scavenge.update_refs=0.00 scavenge.sweep_array_buffers=0.00 background.scavenge.parallel=0.00 background.unmapper=0.04 unmapper=0.00 incremental.steps_count=0 incremental.steps_took=0.0 scavenge_throughput=1752382 total_size_before=261011920 total_size_after=260180920 holes_size_before=838480 holes_size_after=838480 allocated=831000 promoted=0 semi_space_copied=4136 nodes_died_in_new=0 nodes_copied_in_new=0 nodes_promoted=0 promotion_ratio=0.0% average_survival_ratio=0.5% promotion_rate=0.0% semi_space_copy_rate=0.5% new_space_allocation_throughput=887.4 unmapper_chunks=124 [45469:0x150008000] 8918234 ms: pause=0.6 mutator=110.9 gc=s reduce_memory=0 time_to_safepoint=0.00 heap.prologue=0.00 heap.epilogue=0.00 heap.epilogue.reduce_new_space=0.04 heap.external.prologue=0.00 heap.external.epilogue=0.00 heap.external_weak_global_handles=0.00 fast_promote=0.00 complete.sweep_array_buffers=0.00 scavenge=0.50 scavenge.free_remembered_set=0.00 scavenge.roots=0.08 scavenge.weak=0.00 scavenge.weak_global_handles.identify=0.00 scavenge.weak_global_handles.process=0.00 scavenge.parallel=0.08 scavenge.update_refs=0.00 scavenge.sweep_array_buffers=0.00 background.scavenge.parallel=0.00 background.unmapper=0.04 unmapper=0.00 incremental.steps_count=0 incremental.steps_took=0.0 scavenge_throughput=1766409 total_size_before=261207856 total_size_after=260209776 holes_size_before=838480 holes_size_after=838480 allocated=1026936 promoted=0 semi_space_copied=3008 nodes_died_in_new=0 nodes_copied_in_new=0 nodes_promoted=0 promotion_ratio=0.0% average_survival_ratio=0.5% promotion_rate=0.0% semi_space_copy_rate=0.3% new_space_allocation_throughput=888.1 unmapper_chunks=124
5.4 內(nèi)存快照
const { writeHeapSnapshot } = require('node:v8'); v8.writeHeapSnapshot()
打印快照,將會(huì)STW,服務(wù)停止響應(yīng),內(nèi)存占用越大,時(shí)間越長(zhǎng)。此方法本身就比較費(fèi)時(shí)間,所以生成的過(guò)程預(yù)期不要太高,耐心等待。
注意:生成內(nèi)存快照的過(guò)程,會(huì)STW(程序?qū)和#缀鯚o(wú)任何響應(yīng),如果容器使用了健康檢測(cè),這時(shí)無(wú)法響應(yīng)的話,容器可能被重啟,導(dǎo)致無(wú)法獲取快照,如果需要生成快照、建議先關(guān)閉健康檢測(cè)。
兼容性問(wèn)題:此 API arm64 架構(gòu)不支持,執(zhí)行就會(huì)卡住進(jìn)程 生成空快照文件 再無(wú)響應(yīng), 如果使用庫(kù) heapdump,會(huì)直接報(bào)錯(cuò):
(mach-o file, but is an incompatible architecture (have (arm64), need (x86_64))
此 API 會(huì)生成一個(gè) .heapsnapshot 后綴快照文件,可以使用 Chrome 調(diào)試器的“內(nèi)存”功能,導(dǎo)入快照文件,查看堆內(nèi)存具體的對(duì)象數(shù)和大小,以及到GC根結(jié)點(diǎn)的距離等。也可以對(duì)比兩個(gè)不同時(shí)間快照文件的區(qū)別,可以看到它們之間的數(shù)據(jù)量變化。
六、利用內(nèi)存快照分析內(nèi)存泄漏
一個(gè) Node 應(yīng)用因?yàn)閮?nèi)存超過(guò)容器限制經(jīng)常發(fā)生重啟,通過(guò)容器監(jiān)控后臺(tái)看到應(yīng)用內(nèi)存的曲線是一直上升的,那應(yīng)該是發(fā)生了內(nèi)存泄漏。
使用 Chrome 調(diào)試器對(duì)比了不同時(shí)間的快照。發(fā)現(xiàn)對(duì)象增量最多的是閉包函數(shù),繼而展開查看整個(gè)列表,發(fā)現(xiàn)數(shù)據(jù)量較多的是 mongo 文檔對(duì)象,其實(shí)就是閉包函數(shù)內(nèi)的數(shù)據(jù)沒(méi)有被釋放,再通過(guò)查看 Object 列表,發(fā)現(xiàn)同樣很多對(duì)象,最外層的詳情顯示的是 Mongoose 的 Connection 對(duì)象。
到此為止,已經(jīng)大概定位到一個(gè)類的 mongo 數(shù)據(jù)存儲(chǔ)邏輯附近有內(nèi)存泄漏。
再看到 Timeout 對(duì)象也比較多,從 GC 根節(jié)點(diǎn)距離來(lái)看,這些對(duì)象距離非常深。點(diǎn)開詳情,看到這一層層的嵌套就定位到了代碼中準(zhǔn)確的位置。因?yàn)槟莻€(gè)類中有個(gè)定時(shí)任務(wù)使用 setInterval 定時(shí)器去分批處理一些不緊急任務(wù),當(dāng)一個(gè) setInterval 把事情做完之后就會(huì)被 clearInterval 清除。
泄漏解決和優(yōu)化
通過(guò)代碼邏輯分析,最終找到了問(wèn)題所在,是 clearInterval 的觸發(fā)條件有問(wèn)題,導(dǎo)致定時(shí)器沒(méi)有被清除一直循環(huán)下去。定時(shí)器一直執(zhí)行,這段代碼和其中的數(shù)據(jù)還在閉包之中,無(wú)法被 GC 回收,所以內(nèi)存會(huì)越來(lái)越大直至達(dá)到上限崩潰。
這里使用 setInterval 的方式并不合理,順便改成了利用 for await 隊(duì)列順序執(zhí)行,從而達(dá)到避免同時(shí)間大量并發(fā)的效果,代碼也要清晰許多。由于這塊代碼比較久遠(yuǎn),就不考慮為啥當(dāng)初使用 setInterval 了。
發(fā)布新版本之后,觀察了十多天,內(nèi)存平均保持在100M出頭,GC 正?;厥张R時(shí)增長(zhǎng)的內(nèi)存,呈現(xiàn)為波浪曲線,沒(méi)有再出現(xiàn)泄漏。
至此利用內(nèi)存快照,分析并解決了內(nèi)存泄漏。當(dāng)然實(shí)際分析的時(shí)候要曲折一點(diǎn),這個(gè)內(nèi)存快照的內(nèi)容并不好理解、并不那么直接??煺諗?shù)據(jù)的展示是類型聚合的,需要通過(guò)看不同的構(gòu)造函數(shù),以及內(nèi)部的數(shù)據(jù)詳情,結(jié)合自己的代碼綜合分析,才能找到一些線索。 比如從當(dāng)時(shí)我得到的內(nèi)存快照看,有大量數(shù)據(jù)是 閉包、string、mongo model類、Timeout、Object等,其實(shí)這些增量的數(shù)據(jù)都是來(lái)自于那段有問(wèn)題的代碼,并且無(wú)法被 GC 回收。
六、 最后
不同的語(yǔ)言 GC 實(shí)現(xiàn)都不一樣,比如 Java 和 Go:
Java:了解 JVM (對(duì)應(yīng)Node V8)的知道,Java 也采用分代策略,它的新生代中還存在一個(gè) eden 區(qū),新生的對(duì)象都在這個(gè)區(qū)域創(chuàng)建。而 V8 新生代沒(méi)有 eden 區(qū)。
Go:采用標(biāo)記清除,三色標(biāo)記算法
不同的語(yǔ)言的 GC 實(shí)現(xiàn)不同,但是本質(zhì)上都是采用不同算法組合實(shí)現(xiàn)。在性能上,不同的組合,帶來(lái)的各方面性能效率不一樣,但都是此消彼長(zhǎng),只是偏向不同的應(yīng)用場(chǎng)景而已。
更多node相關(guān)知識(shí),請(qǐng)?jiān)L問(wèn):nodejs 教程!
以上是圖文詳解Node V8引擎的內(nèi)存和GC的詳細(xì)內(nèi)容。更多信息請(qǐng)關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

熱AI工具

Undress AI Tool
免費(fèi)脫衣服圖片

Undresser.AI Undress
人工智能驅(qū)動(dòng)的應(yīng)用程序,用于創(chuàng)建逼真的裸體照片

AI Clothes Remover
用于從照片中去除衣服的在線人工智能工具。

Clothoff.io
AI脫衣機(jī)

Video Face Swap
使用我們完全免費(fèi)的人工智能換臉工具輕松在任何視頻中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費(fèi)的代碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
功能強(qiáng)大的PHP集成開發(fā)環(huán)境

Dreamweaver CS6
視覺(jué)化網(wǎng)頁(yè)開發(fā)工具

SublimeText3 Mac版
神級(jí)代碼編輯軟件(SublimeText3)

本篇文章帶大家深入了解NodeJS V8引擎的內(nèi)存和垃圾回收器(GC),希望對(duì)大家有所幫助!

基于無(wú)阻塞、事件驅(qū)動(dòng)建立的Node服務(wù),具有內(nèi)存消耗低的優(yōu)點(diǎn),非常適合處理海量的網(wǎng)絡(luò)請(qǐng)求。在海量請(qǐng)求的前提下,就需要考慮“內(nèi)存控制”的相關(guān)問(wèn)題了。 1. V8的垃圾回收機(jī)制與內(nèi)存限制 Js由垃圾回收機(jī)

事件循環(huán)是 Node.js 的基本組成部分,通過(guò)確保主線程不被阻塞來(lái)實(shí)現(xiàn)異步編程,了解事件循環(huán)對(duì)構(gòu)建高效應(yīng)用程序至關(guān)重要。下面本篇文章就來(lái)帶大家深入了解Node中的事件循環(huán) ,希望對(duì)大家有所幫助!

Go為什么要有GMP調(diào)度模型?下面本篇文章給大家介紹一下Go語(yǔ)言中要有GMP調(diào)度模型的原因,希望對(duì)大家有所幫助!

最近在做接口文檔評(píng)審的時(shí)候,發(fā)現(xiàn)一個(gè)小伙伴定義的出參是個(gè)枚舉值,但是接口文檔沒(méi)有給出對(duì)應(yīng)具體的枚舉值。其實(shí),如何寫好接口文檔,真的很重要。今天田螺哥,給你帶來(lái)接口設(shè)計(jì)文檔的12個(gè)注意點(diǎn)~

在一些底層的庫(kù)中, 經(jīng)常會(huì)看到使用 unsafe 包的地方。本篇文章就來(lái)帶大家了解一下Golang中的unsafe包,介紹一下unsafe 包的作用和Pointer的使用方式,希望對(duì)大家有所幫助!

文件模塊是對(duì)底層文件操作的封裝,例如文件讀寫/打開關(guān)閉/刪除添加等等 文件模塊最大的特點(diǎn)就是所有的方法都提供的**同步**和**異步**兩個(gè)版本,具有 sync 后綴的方法都是同步方法,沒(méi)有的都是異

最開始的時(shí)候 JS 只在瀏覽器端運(yùn)行,對(duì)于 Unicode 編碼的字符串容易處理,但是對(duì)于二進(jìn)制和非 Unicode 編碼的字符串處理困難。并且二進(jìn)制是計(jì)算機(jī)最底層的數(shù)據(jù)格式,視頻/音頻/程序/網(wǎng)絡(luò)包
