這篇文章帶大家深入了解NodeJS V8引擎的記憶體和垃圾回收器(GC),希望對(duì)大家有幫助!
一、為什麼需要GC
#程式運(yùn)行需要使用內(nèi)存,其中記憶體的兩個(gè)分區(qū)是我們常常會(huì)討論的概念:棧區(qū)和堆區(qū)。
堆疊區(qū)是線性的佇列,隨著函數(shù)運(yùn)行結(jié)束自動(dòng)釋放的,而堆區(qū)是自由的動(dòng)態(tài)記憶體空間、堆疊記憶體是手動(dòng)分配釋放或垃圾回收程式(Garbage Collection,後文都簡(jiǎn)稱(chēng)GC)自動(dòng)分配釋放的。
軟體發(fā)展早期或某些語(yǔ)言對(duì)於堆記憶體都是手動(dòng)操作分配和釋放,例如 C、C 。雖然能精準(zhǔn)操作內(nèi)存,達(dá)到盡可能的最優(yōu)內(nèi)存使用,但是開(kāi)發(fā)效率卻非常低,也容易出現(xiàn)內(nèi)存操作不當(dāng)。 【相關(guān)教學(xué)推薦:nodejs影片教學(xué)、程式設(shè)計(jì)教學(xué)】
#隨著科技發(fā)展,高階語(yǔ)言(例如Java Node )都不需要開(kāi)發(fā)者手動(dòng)操作內(nèi)存,程式語(yǔ)言自動(dòng)會(huì)分配和釋放空間。同時(shí)也誕生了 GC(Garbage Collection)垃圾回收器,幫助釋放和整理記憶體。開(kāi)發(fā)者大部分情況不需要關(guān)心記憶體本身,可以專(zhuān)注業(yè)務(wù)開(kāi)發(fā)。後文主要是討論堆記憶體和 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ì)和新建立的物件起衝突。
GC主要是伴隨記憶體大小增加而發(fā)展演化。大致分為3個(gè)大的代表性階段:
- 階段一單執(zhí)行緒GC(代表:serial)
單執(zhí)行緒GC,在它進(jìn)行垃圾收集時(shí),必須完全暫停其他所有的工作執(zhí)行緒 ,它是最初階段的GC,效能也是最糟糕的
- ##階段二並行多執(zhí)行緒GC(代表:Parallel Scavenge, ParNew)
在多CPU 環(huán)境中利用多條GC 執(zhí)行緒同時(shí)並行運(yùn)行,從而垃圾回收的時(shí)間減少、用戶(hù)執(zhí)行緒停頓的時(shí)間也減少,這個(gè)演算法也會(huì)STW,完全暫停其他所有的工作線程
- 階段三多線程並發(fā)concurrent GC(代表:CMS (Concurrent Mark Sweep) G1)
這裡的並發(fā)是指:GC多執(zhí)行緒執(zhí)行可以和業(yè)務(wù)程式碼並發(fā)運(yùn)行。 在前面的兩個(gè)發(fā)展階段的 GC 演算法都會(huì)完全 STW,而在 concurrent GC 中,有部分階段 GC 執(zhí)行緒可以和業(yè)務(wù)程式碼並發(fā)運(yùn)行,保證了更短的 STW 時(shí)間。但這個(gè)模式就會(huì)存在標(biāo)記錯(cuò)誤,因?yàn)镚C 過(guò)程中可能有新物件進(jìn)來(lái),當(dāng)然演算法本身會(huì)修正並解決這個(gè)問(wèn)題上面的三個(gè)階段並不代表GC 一定是上面描述三種的其中一種。不同程式語(yǔ)言的 GC 根據(jù)不同需求採(cǎi)用多種演算法組合實(shí)作。
三、v8 記憶體分區(qū)與GC
堆記憶體設(shè)計(jì)與GC設(shè)計(jì)是緊密相關(guān)的。 V8 把堆記憶體分為幾大區(qū)域,採(cǎi)用分代策略。 盜圖:- 新生代(new-space 或young-generation):空間小,分成了兩個(gè)半空間(semi-space),其中的資料存活期短。
- 老生代(old-space 或old-generation):空間大,可增量,其中的資料存活期長(zhǎng)
- 大物件空間( large-object-space):預(yù)設(shè)超過(guò)256K的物件會(huì)在此空間下,下文解釋
- 程式碼空間(code-space):即時(shí)編譯器(JIT)在這裡儲(chǔ)存已編譯的程式碼
- 元空間(cell space):這個(gè)空間用來(lái)儲(chǔ)存小的、固定大小的JavaScript對(duì)象,像是數(shù)字和布林值。
- 屬性元空間(property cell space):這個(gè)空間用來(lái)儲(chǔ)存特殊的JavaScript對(duì)象,例如存取器屬性和某些內(nèi)部物件。
- Map Space:這個(gè)空間用來(lái)儲(chǔ)存用於JavaScript物件的元資訊和其他內(nèi)部資料結(jié)構(gòu),例如Map和Set物件。
3.1 分代策略:新生代與老生代
3.1.1 新生代
新生代是一個(gè)小的、儲(chǔ)存年齡小的物件、快速的記憶體池,分成了兩個(gè)半空間(semi-space),一半的空間是空閒的(稱(chēng)為to空間),另一半的空間是儲(chǔ)存了資料(稱(chēng)為from空間)。
當(dāng)物件首次建立時(shí),它們被分配到新生代 from 半空間中,它的年齡為1。當(dāng)from 空間不足或超過(guò)一定大小數(shù)量之後,會(huì)觸發(fā)Minor GC(採(cǎi)用複製演算法Scavenge),此時(shí),GC 會(huì)暫停應(yīng)用程式的執(zhí)行(STW,stop-the-world),標(biāo)記(from空間)中所有活動(dòng)對(duì)象,然後將它們整理連續(xù)移動(dòng)到新生代的另一個(gè)空閒空間(to空間)。最後原本的from 空間的記憶體會(huì)被全部釋放而變成空閒空間,兩個(gè)空間就完成from 和to 的對(duì)換,複製演算法是犧牲了空間換取時(shí)間的演算法。
新生代的空間更小,所以此空間會(huì)更頻繁的觸發(fā) GC。同時(shí)掃描的空間更小,GC效能消耗也更小、它的 GC 執(zhí)行時(shí)間也更短。
每當(dāng)一次 Minor GC 完成存活的物件年齡就 1,經(jīng)歷過(guò)多次Minor GC還存活的物件(年齡大於N),它們將被移到老生代記憶體池中。
3.1.2 老生代
老生代是一個(gè)大的記憶體池,用來(lái)儲(chǔ)存較長(zhǎng)壽命的物件。老生代記憶體採(cǎi)用 標(biāo)記清除(Mark-Sweep)、標(biāo)記壓縮演算法(Mark-Compact)。它的一次執(zhí)行叫做 Mayor GC。當(dāng)老生代中的物件佔(zhàn)滿(mǎn)一定比例時(shí),即存活物件與總物件的比例超過(guò)一定的閾值,就會(huì)觸發(fā)一次 標(biāo)記清除 或 標(biāo)記壓縮。
因?yàn)樗目臻g更大,它的GC執(zhí)行時(shí)間也更長(zhǎng),頻率相對(duì)新生代更低。如果老生代完成 GC 回收之後空間還是不足,V8 就會(huì)從系統(tǒng)中申請(qǐng)更多記憶體。
可以手動(dòng)執(zhí)行 global.gc() 方法,設(shè)定不同參數(shù),主動(dòng)觸發(fā)GC。 但是要注意的是,預(yù)設(shè)情況下,Node.js 是禁用了此方法。如果要啟用,可以透過(guò)啟動(dòng)Node.js 應(yīng)用程式時(shí)新增--expose-gc 參數(shù)來(lái)開(kāi)啟,例如:
node --expose-gc app.js
V8 在老生代中主要採(cǎi)用了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)記清除回收後,記憶體空間會(huì)出現(xiàn)不連續(xù)的狀態(tài)。這種記憶體碎片會(huì)對(duì)後續(xù)的記憶體分配造成問(wèn)題,因?yàn)楹芸赡艹霈F(xiàn)需要分配一個(gè)大物件的情況,這時(shí)所有的碎片空間都無(wú)法完成此次分配,就會(huì)提前觸發(fā)垃圾回收,而這次回收是不必要的。
為了解決 Mark-Sweep 的記憶體碎片問(wèn)題,Mark-Compact 被提出來(lái)。 Mark-Compact 是標(biāo)記整理的意思,是在 Mark-Sweep 的基礎(chǔ)上演進(jìn)而來(lái)的。它們的差異在於物件在標(biāo)記為死亡後,在整理過(guò)程中,將活著的物件往一端移動(dòng),移動(dòng)完成後,直接清除邊界外的記憶體。 V8 也會(huì)根據(jù)某個(gè)邏輯,釋放一定空閒的記憶體還給系統(tǒng)。
3.2 大物件空間 large object space
大物件會(huì)直接在大物件空間創(chuàng)建,並且不會(huì)移動(dòng)到其它空間。那麼到底多大的物件會(huì)直接在大物件空間創(chuàng)建,而不是在新生代 from 區(qū)中創(chuàng)建呢?查閱資料和原始碼終於找到了答案。預(yù)設(shè)是 256K,V8 似乎並沒(méi)有暴露修改指令,原始碼中的 v8_enable_hugepage 設(shè)定應(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(如果開(kāi)啟了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)存信息,查詢(xún)最大堆內(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 容器中查詢(xún) 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í)獲取此接口,然后繪畫(huà)出折線圖幫助分析內(nèi)存占用情況。以下是 Easy-Monitor 提供的功能:
建議本地開(kāi)發(fā)環(huán)境使用,開(kāi)啟后,嘗試大量請(qǐng)求,會(huì)看到內(nèi)存曲線增長(zhǎng),到請(qǐng)求結(jié)束之后,GC觸發(fā)后會(huì)看到內(nèi)存曲線下降,然后再?lài)L試多次發(fā)送大量請(qǐng)求,這樣往復(fù)下來(lái),如果發(fā)現(xiàn)內(nèi)存一直在增長(zhǎng)低谷值越來(lái)越高,就可能是發(fā)生了內(nèi)存泄漏。
5.3 開(kāi)啟打印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類(lèi)型,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類(lèi)型,各種時(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ù),繼而展開(kāi)查看整個(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è)類(lèi)的 mongo 數(shù)據(jù)存儲(chǔ)邏輯附近有內(nèi)存泄漏。
再看到 Timeout 對(duì)象也比較多,從 GC 根節(jié)點(diǎn)距離來(lái)看,這些對(duì)象距離非常深。點(diǎn)開(kāi)詳情,看到這一層層的嵌套就定位到了代碼中準(zhǔn)確的位置。因?yàn)槟莻€(gè)類(lèi)中有個(gè)定時(shí)任務(wù)使用 setInterval 定時(shí)器去分批處理一些不緊急任務(wù),當(dāng)一個(gè) setInterval 把事情做完之后就會(huì)被 clearInterval 清除。
洩漏解決和最佳化
透過(guò)程式碼邏輯分析,最終找到了問(wèn)題所在,是clearInterval 的觸發(fā)條件有問(wèn)題,導(dǎo)致計(jì)時(shí)器沒(méi)有被清除一直循環(huán)下去。定時(shí)器一直執(zhí)行,這段程式碼和其中的資料還在閉包之中,無(wú)法被 GC 回收,所以記憶體會(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)洩漏。
至此利用記憶體快照,分析並解決了記憶體洩漏。當(dāng)然實(shí)際分析的時(shí)候要曲折一點(diǎn),這個(gè)記憶體快照的內(nèi)容並不好理解、沒(méi)那麼直接??煺召Y料的展示是類(lèi)型聚合的,需要透過(guò)看不同的建構(gòu)函數(shù),以及內(nèi)部的資料詳情,結(jié)合自己的程式碼綜合分析,才能找到一些線索。 例如從當(dāng)時(shí)我得到的記憶體快照看,有大量資料是閉包、string、mongo model類(lèi)別、Timeout、Object等,其實(shí)這些增量的資料都是來(lái)自於那段有問(wèn)題的程式碼,並且無(wú)法被GC 回收。
六、 最後
不同的語(yǔ)言GC 實(shí)作都不一樣,例如Java 和Go:
Java:了解JVM (對(duì)應(yīng)Node V8)的知道,Java 也採(cǎi)用分代策略,它的新生代中還存在一個(gè)eden 區(qū),新生的物件都在這個(gè)區(qū)域創(chuàng)建。而 V8 新生代沒(méi)有 eden 區(qū)。
Go:採(cǎi)用標(biāo)記清除,三色標(biāo)記演算法
不同的語(yǔ)言的 GC 實(shí)作不同,但本質(zhì)上都是採(cǎi)用不同演算法組合實(shí)作。在效能上,不同的組合,帶來(lái)的各方面效能效率都不一樣,但都是此消彼長(zhǎng),只是偏向不同的應(yīng)用場(chǎng)景而已。
更多node相關(guān)知識(shí),請(qǐng)?jiān)煸L:nodejs 教學(xué)!
以上是圖文詳解Node V8引擎的記憶體和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脫衣器

Video Face Swap
使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門(mén)文章

熱工具

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

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

禪工作室 13.0.1
強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

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

SublimeText3 Mac版
神級(jí)程式碼編輯軟體(SublimeText3)

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

事件循環(huán)是 Node.js 的基本組成部分,透過(guò)確保主執(zhí)行緒不被阻塞來(lái)實(shí)現(xiàn)非同步編程,了解事件循環(huán)對(duì)建立高效應(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í),如何寫(xiě)好介面文檔,真的很重要。今天田螺哥,帶給你介面設(shè)計(jì)文件的12個(gè)注意點(diǎn)~

一開(kāi)始的時(shí)候 JS 只在瀏覽器端運(yùn)行,對(duì)於 Unicode 編碼的字串容易處理,但對(duì)於二進(jìn)位和非 Unicode 編碼的字串處理困難。並且二進(jìn)制是電腦最底層的資料格式,視訊/音訊/程式/網(wǎng)路包

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

Django:前端和後端開(kāi)發(fā)都能搞定的神奇框架! Django是一個(gè)高效、可擴(kuò)展的網(wǎng)路應(yīng)用程式框架。它能夠支援多種Web開(kāi)發(fā)模式,包括MVC和MTV,可以輕鬆地開(kāi)發(fā)出高品質(zhì)的Web應(yīng)用程式。 Django不僅支援後端開(kāi)發(fā),還能夠快速建構(gòu)出前端的介面,透過(guò)模板語(yǔ)言,實(shí)現(xiàn)靈活的視圖展示。 Django把前端開(kāi)發(fā)和後端開(kāi)發(fā)融合成了一種無(wú)縫的整合,讓開(kāi)發(fā)人員不必專(zhuān)門(mén)學(xué)習(xí)

這篇文章帶給大家的內(nèi)容是介紹深入理解golang中的泛型?泛型怎麼使用?有一定的參考價(jià)值,有需要的朋友可以參考一下,希望對(duì)你們有幫助。
