<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      NodeJS V8引擎的內存和垃圾回收器(GC)

      一、為什么需要GC

      程序應用運行需要使用內存,其中內存的兩個分區是我們常常會討論的概念:棧區和堆區。

      棧區是線性的隊列,隨著函數運行結束自動釋放的,而堆區是自由的動態內存空間、堆內存是手動分配釋放或者 垃圾回收程序(Garbage Collection,后文都簡稱GC)自動分配釋放的。

      軟件發展早期或者一些語言對于堆內存都是手動操作分配和釋放,比如 CC++。雖然能精準操作內存,達到盡可能的最優內存使用,但是開發效率卻非常低,也容易出現內存操作不當。

      隨著技術發展,高級語言(例如Java Node)都不需要開發者手動操作內存,程序語言自動會分配和釋放空間。同時也誕生了 GC(Garbage Collection)垃圾回收器,幫助釋放和整理內存。開發者大部分情況不需要關心內存本身,可以專注業務開發。后文主要是討論堆內存和 GC

      二、GC發展

      GC運行會消耗CPU資源,GC運行的過程會觸發STW(stop-the-world)暫停業務代碼線程,為什么會 STW 呢?是為了保證在 GC 的過程中,不會和新創建的對象起沖突。

      GC主要是伴隨內存大小增加而發展演化。大致分為3個大的代表性階段:

      • 階段一 單線程GC(代表:serial)

      單線程GC,在它進行垃圾收集時,必須完全暫停其他所有的工作線程 ,它是最初階段的GC,性能也是最差的

      • 階段二 并行多線程GC(代表:Parallel Scavenge, ParNew)

      在多 CPU 環境中利用多條 GC 線程同時并行運行,從而垃圾回收的時間減少、用戶線程停頓的時間也減少,這個算法也會STW,完全暫停其他所有的工作線程

      • 階段三 多線程并發 concurrent GC(代表:CMS (Concurrent Mark Sweep) G1)

      這里的并發是指:GC多線程執行可以和業務代碼并發運行。

      在前面的兩個發展階段的 GC 算法都會完全 STW,而在 concurrent GC 中,有部分階段 GC 線程可以和業務代碼并發運行,保證了更短的 STW 時間。但是這個模式就會存在標記錯誤,因為 GC 過程中可能有新對象進來,當然算法本身會修正和解決這個問題

      上面的三個階段并不代表 GC 一定是上面描述三種的其中一種。不同程序語言的 GC 根據不同需求采用多種算法組合實現。

      三、v8 內存分區與GC

      堆內存設計與GC設計是緊密相關的。V8 把堆內存分為幾大區域,采用分代策略。

      盜圖:

      image.png

      • 新生代(new-space 或 young-generation):空間小,分為了兩個半空間(semi-space),其中的數據存活期短。
      • 老生代(old-space 或 old-generation):空間大,可增量,其中的數據存活期長
      • 大對象空間(large-object-space):默認超過256K(64位) 128K(32位)的對象會在此空間下,下文解釋
      • 代碼空間(code-space):即時編譯器(JIT)在這里存儲已編譯的代碼
      • 元空間 (cell space):這個空間用于存儲小的、固定大小的JavaScript對象,比如數字和布爾值。
      • 屬性元空間 (property cell space):這個空間用于存儲特殊的JavaScript對象,比如訪問器屬性和某些內部對象。
      • Map Space:這個空間用于存儲用于JavaScript對象的元信息和其他內部數據結構,比如Map和Set對象。

      3.1 分代策略:新生代和老生代

      新老生代.png

      在 Node.js 中,GC 采用分代策略,分為新、老生代區,內存數據大都在這兩個區域。

      3.1.1 新生代

      新生代是一個小的、存儲年齡小的對象、快速的內存池,分為了兩個半空間(semi-space),一半的空間是空閑的(稱為to空間),另一半的空間是存儲了數據(稱為from空間)。

      當對象首次創建時,它們被分配到新生代 from 半空間中,它的年齡為1。當 from 空間不足或者超過一定大小數量之后,會觸發 Minor GC(采用復制算法 Scavenge),此時,GC 會暫停應用程序的執行(STW,stop-the-world),標記(from空間)中所有活動對象,然后將它們整理連續移動到新生代的另一個空閑空間(to空間)中。最后原本的 from 空間的內存會被全部釋放而變成空閑空間,兩個空間就完成 fromto 的對換,復制算法是犧牲了空間換取時間的算法。

      新生代的空間更小,所以此空間會更頻繁的觸發 GC。同時掃描的空間更小,GC性能消耗也更小、它的 GC 執行時間也更短。

      每當一次 Minor GC 完成存活的對象年齡就+1,經歷過多次Minor GC還存活的對象(年齡大于N),它們將被移動到老生代內存池中。

      3.1.2 老生代

      老生代是一個大的內存池,用于存儲較長壽命的對象。老生代內存采用 標記清除(Mark-Sweep)標記壓縮算法(Mark-Compact)。它的一次執行叫做 Mayor GC。當老生代中的對象占滿一定比例時,即存活對象與總對象的比例超過一定的閾值,就會觸發一次 標記清除標記壓縮

      因為它的空間更大,它的GC執行時間也更長,頻率相對新生代更低。如果老生代完成 GC 回收之后空間還是不足,V8 就會從系統中申請更多內存。

      可以手動執行 global.gc() 方法,設置不同參數,主動觸發GC。
      但是需要注意的是,默認情況下,Node.js 是禁用了global.gc()。如果要啟用,可以通過啟動 Node.js 應用程序時添加 --expose-gc 參數來開啟,例如:

      node --expose-gc app.js
      

      V8 在老生代中主要采用了 Mark-SweepMark-Compact 相結合的方式進行垃圾回收。

      Mark-Sweep 是標記清除的意思,它分為兩個階段,標記和清除。Mark-Sweep 在標記階段遍歷堆中的所有對象,并標記活著的對象,在隨后的清除階段中,只清除未被標記的對象。

      Mark-Sweep 最大的問題是在進行一次標記清除回收后,內存空間會出現不連續的狀態。這種內存碎片會對后續的內存分配造成問題,因為很可能出現需要分配一個大對象的情況,這時所有的碎片空間都無法完成此次分配,就會提前觸發垃圾回收,而這次回收是不必要的。

      為了解決 Mark-Sweep 的內存碎片問題,Mark-Compact 被提出來。Mark-Compact 是標記整理的意思,是在 Mark-Sweep 的基礎上演進而來的。它們的差別在于對象在標記為死亡后,在整理過程中,將活著的對象往一端移動,移動完成后,直接清理掉邊界外的內存。V8 也會根據一定邏輯,釋放一定空閑的內存還給系統。

      3.2 大對象空間 large object space

      大對象會直接在大對象空間創建,并且不會移動到其它空間。那么到底多大的對象會直接在大對象空間創建,而不是在新生代 from 區中創建呢?查閱資料和源代碼終于找到了答案。默認情況下是 64位系統256K,32位系統128K,V8 似乎并沒有暴露修改命令,源碼中的 v8_enable_hugepage 配置應該是打包的時候設定的。

      https://chromium.googlesource.com/v8/v8.git/+/5.1.281.35/src/heap/spaces.h

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

      https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;l=538;drc=cb95e30fa939a18bc0845b57b0946a102b86cf9d?q=kmaxregular&ss=chromium%2Fchromium%2Fsrc

      1.png

      image.png

      (1 << (18 - 1)) 的結果 256K
      (1 << (19 - 1)) 的結果 256K
      (1 << (21 - 1)) 的結果 1M(如果開啟了hugPage)
      

      四、V8 新老分區大小

      4.1 老生代分區大小

      在v12.x 之前:

      為了保證 GC 的執行時間保持在一定范圍內,V8 限制了最大內存空間,設置了一個默認老生代內存最大值,64位系統中為大約1.4G,32位為大約700M,超出會導致應用崩潰。

      如果想加大內存,可以使用 --max-old-space-size 設置最大內存(單位:MB)

      node --max_old_space_size=
      

      在v12以后:

      V8 將根據可用內存分配老生代大小,也可以說是堆內存大小,所以并沒有限制堆內存大小。以前的限制邏輯,其實不合理,限制了 V8 的能力,總不能因為 GC 過程消耗的時間更長,就不讓我繼續運行程序吧,后續的版本也對 GC 做了更多優化,內存越來越大也是發展需要。

      如果想要做限制,依然可以使用 --max-old-space-size 配置, v12 以后它的默認值是0,代表不限制。

      參考文檔:
      https://nodejs.medium.com/introducing-node-js-12-76c41a1b3f3f

      4.2 新生代分區大小

      新生代中的一個 semi-space 大小 64位系統的默認值是16M,32位系統是8M,因為有2個 semi-space,所以總大小是32M、16M。

      --max-semi-space-size

      --max-semi-space-size 設置新生代 semi-space 最大值,單位為MB。

      此空間不是越大越好,空間越大掃描的時間就越長。這個分區大部分情況下是不需要做修改的,除非針對具體的業務場景做優化,謹慎使用。

      --max-new-space-size

      --max-new-space-size 設置新生代空間最大值,單位為KB(不存在)

      有很多文章說到此功能,我翻了下 nodejs.org 網頁中 v4 v6 v7 v8 v10的文檔都沒有看到有這個配置,使用 node --v8-options 也沒有查到,也許以前的某些老版本有,而現在都應該使用 --max-semi-space-size

      五、 內存分析相關API

      5.1 v8.getHeapStatistics()

      執行 v8.getHeapStatistics(),查看 v8 堆內存信息,查詢最大堆內存 heap_size_limit,當然這里包含了新、老生代、大對象空間等。我的電腦硬件內存是 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 應用,分別查看了v12 v14 v16版本,如下表。看起來是本身系統當前的最大內存的一半。128M 的時候,為啥是 256M,因為容器中還有交換內存,容器內存實際最大內存限制是內存限制值 x2,有同等的交換內存。

      所以結論是大部分情況下 heap_size_limit 的默認值是系統內存的一半。但是如果超過這個值且系統空間足夠,V8 還是會申請更多空間。當然這個結論也不是一個最準確的結論。而且隨著內存使用的增多,如果系統內存還足夠,這里的最大內存還會增長。

      容器最大內存 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
      }
      

      通過它可以查看當前進程的內存占用和使用情況 heapTotalheapUsed,可以定時獲取此接口,然后繪畫出折線圖幫助分析內存占用情況。以下是 Easy-Monitor 提供的功能:

      image.png

      建議本地開發環境使用,開啟后,嘗試大量請求,會看到內存曲線增長,到請求結束之后,GC觸發后會看到內存曲線下降,然后再嘗試多次發送大量請求,這樣往復下來,如果發現內存一直在增長低谷值越來越高,就可能是發生了內存泄漏。

      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
      

      上面的 ScavengeMark-sweep 代表GC類型,Scavenge 是新生代中的清除事件,Mark-sweep 是老生代中的標記清除事件。箭頭符號前是事件發生前的實際使用內存大小,箭頭符號后是事件結束后的實際使用內存大小,括號內是內存空間總值。可以看到新生代中事件發生的頻率很高,而后觸發的老生代事件會釋放總內存空間。

      • --trace_gc_verbose

      展示堆空間的詳細情況

      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事件的詳細信息,GC類型,各種時間消耗,內存變化等

      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 內存快照

      const { writeHeapSnapshot } = require('node:v8');
      v8.writeHeapSnapshot()
      

      打印快照,將會STW,服務停止響應,內存占用越大,時間越長。此方法本身就比較費時間,所以生成的過程預期不要太高,耐心等待。

      注意:生成內存快照的過程,會STW(程序將暫停)幾乎無任何響應,如果容器使用了健康檢測,這時無法響應的話,容器可能被重啟,導致無法獲取快照,如果需要生成快照、建議先關閉健康檢測。

      兼容性問題:此 API arm64 架構不支持,執行就會卡住進程 生成空快照文件 再無響應,
      如果使用庫 heapdump,會直接報錯:

      (mach-o file, but is an incompatible architecture (have (arm64), need (x86_64))

      API 會生成一個 .heapsnapshot 后綴快照文件,可以使用 Chrome 調試器的“內存”功能,導入快照文件,查看堆內存具體的對象數和大小,以及到GC根結點的距離等。也可以對比兩個不同時間快照文件的區別,可以看到它們之間的數據量變化。

      六、利用內存快照分析內存泄漏

      一個 Node 應用因為內存超過容器限制經常發生重啟,通過容器監控后臺看到應用內存的曲線是一直上升的,那應該是發生了內存泄漏。

      使用 Chrome 調試器對比了不同時間的快照。發現對象增量最多的是閉包函數,繼而展開查看整個列表,發現數據量較多的是 mongo 文檔對象,其實就是閉包函數內的數據沒有被釋放,再通過查看 Object 列表,發現同樣很多對象,最外層的詳情顯示的是 MongooseConnection 對象。

      image.png

      image.png

      到此為止,已經大概定位到一個類的 mongo 數據存儲邏輯附近有內存泄漏。

      再看到 Timeout 對象也比較多,從 GC 根節點距離來看,這些對象距離非常深。點開詳情,看到這一層層的嵌套就定位到了代碼中準確的位置。因為那個類中有個定時任務使用 setInterval 定時器去分批處理一些不緊急任務,當一個 setInterval 把事情做完之后就會被 clearInterval 清除。

      image.png
      image.png

      泄漏解決和優化

      通過代碼邏輯分析,最終找到了問題所在,是 clearInterval 的觸發條件有問題,導致定時器沒有被清除一直循環下去。定時器一直執行,這段代碼和其中的數據還在閉包之中,無法被 GC 回收,所以內存會越來越大直至達到上限崩潰。

      這里使用 setInterval 的方式并不合理,順便改成了利用 for await 隊列順序執行,從而達到避免同時間大量并發的效果,代碼也要清晰許多。由于這塊代碼比較久遠,就不考慮為啥當初使用 setInterval 了。

      發布新版本之后,觀察了十多天,內存平均保持在100M出頭,GC 正常回收臨時增長的內存,呈現為波浪曲線,沒有再出現泄漏。

      image.png

      至此利用內存快照,分析并解決了內存泄漏。當然實際分析的時候要曲折一點,這個內存快照的內容并不好理解、并不那么直接。快照數據的展示是類型聚合的,需要通過看不同的構造函數,以及內部的數據詳情,結合自己的代碼綜合分析,才能找到一些線索。
      比如從當時我得到的內存快照看,有大量數據是 閉包、string、mongo model類、Timeout、Object等,其實這些增量的數據都是來自于那段有問題的代碼,并且無法被 GC 回收。

      六、 最后

      不同的語言 GC 實現都不一樣,比如 JavaGo

      Java:了解 JVM (對應Node V8)的知道,Java 也采用分代策略,它的新生代中還存在一個 eden 區,新生的對象都在這個區域創建。而 V8 新生代沒有 eden 區。

      Go:采用標記清除,三色標記算法

      不同的語言的 GC 實現不同,但是本質上都是采用不同算法組合實現。在性能上,不同的組合,帶來的各方面性能效率不一樣,但都是此消彼長,只是偏向不同的應用場景而已。

      posted @ 2023-03-29 08:16  子慕大詩人  閱讀(1633)  評論(1)    收藏  舉報
      主站蜘蛛池模板: 精品乱人码一区二区二区| 新平| 亚洲深深色噜噜狠狠网站| 久久伊99综合婷婷久久伊| 51午夜精品免费视频| 国产一区二区不卡91| 18禁无遮拦无码国产在线播放 | 国产色爱av资源综合区| 国产精品无码av天天爽播放器| 日韩一区二区三区无码a片| 成人午夜伦理在线观看| 激情 小说 亚洲 图片 伦| 开心激情站开心激情网六月婷婷| 色就色偷拍综合一二三区| 精品人妻伦九区久久aaa片| 艳妇臀荡乳欲伦交换h在线观看| 人妻精品久久无码区| 日本美女性亚洲精品黄色| 国产精品激情av在线播放| 国产一区二区不卡在线| 亚洲精品一区二区二三区| 成人国产精品中文字幕| 国产亚洲精久久久久久无码77777| 啪啪av一区二区三区| 人妻少妇精品视频三区二区| 中文字幕在线观看一区二区| 欧美亚洲熟妇一区二区三区| 日韩人妻久久精品一区二区 | 亚洲欧美国产免费综合视频| 日韩不卡在线观看视频不卡| аⅴ天堂中文在线网| 不卡乱辈伦在线看中文字幕| 免费A级毛片无码A∨蜜芽试看 | 韩国三级在线 中文字幕 无码| 欧美三级在线播放| 奶头好大揉着好爽视频| 樱花草视频www日本韩国| 色狠狠一区二区三区香蕉| 久久精品国产亚洲av高| 亚洲精品美女一区二区| 一区二区福利在线视频|