亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

目錄
一、為什麼需要GC
二、GC發(fā)展
3.1.1 新生代
3.1.2 老生代
3.2 大物件空間 large object space
四、V8 新老分區(qū)大小
4.1 老生代分區(qū)大小
4.2 新生代分區(qū)大小
五、 內(nèi)存分析相關(guān)API
5.1 v8.getHeapStatistics()
5.2 process.memoryUsage
5.3 開(kāi)啟打印GC事件
5.4 內(nèi)存快照
六、利用內(nèi)存快照分析內(nèi)存泄漏
洩漏解決和最佳化
六、 最後
首頁(yè) web前端 js教程 圖文詳解Node V8引擎的記憶體和GC

圖文詳解Node V8引擎的記憶體和GC

Mar 29, 2023 pm 06:02 PM
node.js 後端 v8

這篇文章帶大家深入了解NodeJS V8引擎的記憶體和垃圾回收器(GC),希望對(duì)大家有幫助!

圖文詳解Node V8引擎的記憶體和GC

一、為什麼需要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)用分代策略。

盜圖:

圖文詳解Node V8引擎的記憶體和GC

  • 新生代(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 分代策略:新生代與老生代

圖文詳解Node V8引擎的記憶體和GC

#在Node.js 中,GC 採(cǎi)用分代策略,分為新、老生代區(qū),記憶體資料大都在這兩個(gè)區(qū)域。

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è)空間就完成fromto 的對(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 -SweepMark-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è)是 256KV8 似乎並沒(méi)有暴露修改指令,原始碼中的 v8_enable_hugepage 設(shè)定應(yīng)該是打包的時(shí)候設(shè)定的。

chromium.googlesource.com/v8/v8.git/ …

 // 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.

source.chromium.org/ chromium/ch…

圖文詳解Node V8引擎的記憶體和GC

#

圖文詳解Node V8引擎的記憶體和GC

(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
4G2G
2G1G
1G0.5G
1.5G0.7G
256M256M
128M256M

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 提供的功能:

圖文詳解Node V8引擎的記憶體和GC

建議本地開(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(&#39;--trace_gc&#39;);
  • --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

上面的 ScavengeMark-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(&#39;--trace_gc_verbose&#39;);

[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(&#39;--trace_gc_nvp&#39;);

[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(&#39;node:v8&#39;);
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ì)象,最外層的詳情顯示的是 MongooseConnection 對(duì)象。

圖文詳解Node V8引擎的記憶體和GC

圖文詳解Node V8引擎的記憶體和GC

到此為止,已經(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 清除。

圖文詳解Node V8引擎的記憶體和GC圖文詳解Node V8引擎的記憶體和GC

洩漏解決和最佳化

透過(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)洩漏。

圖文詳解Node V8引擎的記憶體和GC

至此利用記憶體快照,分析並解決了記憶體洩漏。當(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í)作都不一樣,例如JavaGo

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)文章!

本網(wǎng)站聲明
本文內(nèi)容由網(wǎng)友自願(yuàn)投稿,版權(quán)歸原作者所有。本站不承擔(dān)相應(yīng)的法律責(zé)任。如發(fā)現(xiàn)涉嫌抄襲或侵權(quán)的內(nèi)容,請(qǐng)聯(lián)絡(luò)admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費(fèi)脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

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

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

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

Dreamweaver CS6

Dreamweaver CS6

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

SublimeText3 Mac版

SublimeText3 Mac版

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

一文聊聊Node中的記憶體控制 一文聊聊Node中的記憶體控制 Apr 26, 2023 pm 05:37 PM

基於無(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ī)

一起聊聊Node中的事件循環(huán) 一起聊聊Node中的事件循環(huán) Apr 11, 2023 pm 07:08 PM

事件循環(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語(yǔ)言中要有GMP調(diào)度模型的原因 深入淺析Go語(yǔ)言中要有GMP調(diào)度模型的原因 Apr 14, 2023 pm 03:26 PM

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

分享介面設(shè)計(jì)文件的12個(gè)注意點(diǎn) 分享介面設(shè)計(jì)文件的12個(gè)注意點(diǎn) Apr 24, 2023 am 10:58 AM

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

深入了解Node中的Buffer 深入了解Node中的Buffer Apr 25, 2023 pm 07:49 PM

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

深入聊聊Node中的File模組 深入聊聊Node中的File模組 Apr 24, 2023 pm 05:49 PM

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

Django:前端和後端開(kāi)發(fā)都能搞定的神奇框架! Django:前端和後端開(kāi)發(fā)都能搞定的神奇框架! Jan 19, 2024 am 08:52 AM

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í)

深入了解golang中的泛型(Generic) 深入了解golang中的泛型(Generic) Apr 11, 2023 pm 07:20 PM

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

See all articles