日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Node的垃圾回收机制与内存溢出捕获(上)

發布時間:2025/7/14 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Node的垃圾回收机制与内存溢出捕获(上) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Node的垃圾回收機制與內存溢出捕獲

一、什么是Node的內存?

??想必大家在用JavaScript開發的過程中,不太關心內存的管理,因為對于前端來說,瀏覽器的內存幾乎不會出現用完的情況,因為所接觸的是那些短時間執行的場景,比如網頁的應用、命令工具等。這類場景由于是運行短時間,且運行在用戶的機器上,即使內存被消耗過多或者內存發生了泄漏,已只會影響到終端用戶,并不會大面積的擴散。因此運行時間短,隨著進程的退出,內存會自動釋放,幾乎沒有內存管理的必要。

1.Node的內存需要管理嗎?

??答案是必須的。為啥呢?

??因為Node作為后端服務,操作復雜,并且長期運行在服務器端不重啟。如果不關注內存管理,將會導致內存泄漏,就算是1TB,也會很快會被耗盡。

2.Node的內存究竟是什么樣的呢?

2.1 Node是在什么環境下運行的呢?

?? 回溯歷史可以發現,Node在發展的歷程中離不開Chrome V8 (ps:下面會提到什么是V8),所以在官方的主頁大家可以看到Node是一個構建在Chrome的 JavaScript運行上的平臺(++Node.js? is a JavaScript runtime built on Chrome's V8 JavaScript engine.++)。換句話說,其實Node.js就是一個由JavaScript V8引擎控制的C++程序。

?? Google V8是一個由Google開發的JavaScript引擎,但它也可以脫離瀏覽器被單獨使用。 這使得它能夠完美的契合Node.js,實際上V8也是Node.js平臺中唯一能夠理解JavaScript的部分。 V8會將JavaScript代碼向下編譯為本地代碼(native code),然后執行它。在執行期間,V8會按需進行內存的分配和釋放。 這意味著,如果我們在談論Node.js的內存管理問題,也就是在說V8的內存管理問題。

2.2 V8的內存管理模式
2.2.1 V8的內存設計

?? 一個運行的程序通常是通過在內存中分配一部分空間來表示的。這部分空間被稱為常駐內存(Resident Set)。

?? V8的內存管理模式有點類似于Java虛擬機(JVM),它會將內存進行分段:

  • 代碼區(Code Segment):存放即將執行的代碼片段
  • 棧 Stack:包括所有的攜帶指針引用堆上對象的值類型(原始類型,例如整型和布爾),以及定義程序控制流的指針。
  • 堆 Heap:用于保存引用類型(包括對象、字符串和閉包)的內存段。
  • 堆外內存:不通過V8分配,也不受V8管理。Buffer對象的數據就存放于此。

2.2.2 V8內存模型

?? 除堆外內存,其余部分均由V8管理。

  • 棧(Stack)的分配與回收非常直接,當程序離開某作用域后,其棧指針下移(回退),整個作用域的局部變量都會出棧,內存收回。
  • 最復雜的部分是堆(Heap)的管理,V8使用垃圾回收機制進行堆的內存管理,也是開發中可能造成內存泄漏的部分,是我們需要關注的重點。

在Node.js中,當前的內存使用情況可以輕松的使用process.memoryUsage()進行查詢, 實例程序如下:

$ node $ process.memoryUsage() 復制代碼

這是公司內部的一個項目的Node進程的內存使用狀況:

  • rss是Resident Set Size的縮寫,為常駐內存的總大小(單位:bytes),大約21M。

  • heapTotal是V8為堆分配的總大小(單位:bytes),大約9.23M。

  • heapUsed是已使用的堆大小(單位:bytes),大約5.29M。

可以看到,rss是大于heapTotal的,因為rss包括且不限于堆。

  • external是堆外內存大小(單位:bytes),0.0085M。

當我們在代碼中聲明變量并賦值的時候,所使用對象的內存就分配在堆中。如果已申請的堆空間內存不夠分配新的對象,將繼續申請內存,直到堆的大小超過V8的限制為止。

2.2.3 V8內存限制

?? V8內存為何要限制大小呢?V8不就是為了瀏覽器設計的么,瀏覽器中不太可能遇到太大的內存場景,對于一般正常瀏覽網頁來說,停留的時間不會太長,也不太會進行很多復雜的工作,照理說V8內存的限制已經綽綽有余了。但是遇到大內存的時候,比如讀取大的文件進內存,那要怎么辦呢?

?? 其實引起V8內存限制的深層次原因是其垃圾回收機制的限制。舉個栗子,官方說法是,以1.5GB的垃圾回收堆內存為例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引起JavaScript線程暫停執行的時間,在這樣的花銷下,應用性能和相應時間能力都會直線下降。這樣的情況不僅僅后端服務無法接受,前端瀏覽器也無法接受。因此,是時候需要考慮一下是否改變內存的閥值了。

?? 在啟動node進程的時候,可以調整內存大小。

node --max-old-space-size=1700 test.js // 單位為MB node --max-new-space-size=1024 test.js // 單位為KB 復制代碼

?? 上述參數在初始化進程的時候就生效,一旦生效就不能動態擴容,一般用來擴充內存,以免稍微多一些內存就崩潰。

2.2.4 V8的內存分代

?? V8垃圾回收策略主要基于分代垃圾回收機制。在實際應用過程中發現,對象的生存周期長短不一,因此只能按照對象的存活時間將內存的垃圾回收進行不同的分代。

?? 在V8中,主要將內存分為 新生代 和 老生代。新生代中的對象為存活時間較短的對象,老生代中的對象為存活時間較長的或常駐內存的對象。

V8堆的整體大小就是新生代所用內存空間加上老生代的內存空間。就是上面所提到的用--max-old-space-size來設置老生代內存空間的最大值,--max-new-space-size來設置新生代內存空間的最大值。

?? v8源碼中,我們可以看到這個說明,在代碼Page::kPageSize=1下:

// semispace_size_ should be a power of 2 and old_generation_size_ should be // a multiple of Page::kPageSize #if defined(V8_TARGET_ARCH_X64) #define LUMP_OF_MEMORY(2 * MB)code_range_size_(512 * MB), #else #define LUMP_OF_MEMORY MBcode_range_size_(0), #endif #if defined(ANDROID)reserved_semispace_size_(4 * Max(LUMP_OF_MEMORY, Page:: kPageSize)),max_semispace_size_(4 * Max(LUMP_OF_MEMORY, Page:: kPageSize)),initial_semispace_size_(Page:: kPageSize),max_old_generation_size_(192 * MB),max_executable_size_(max_old_generation_size_), #elsereserved_semispace_size_(8 * Max(LUMP_OF_MEMORY, Page:: kPageSize)),max_semispace_size_(8 * Max(LUMP_OF_MEMORY, Page:: kPageSize)),initial_semispace_size_(Page:: kPageSize),max_old_generation_size_(700ul * LUMP_OF_MEMORY),max_executable_size_(256l * LUMP_OF_MEMORY), #endif 復制代碼

?? 依照上面的代碼,我們可以看到如果V8標記是64位系統的需要*2,32位的不需要。

?? 對于新生代來說,它是由兩個reserved_semispace_size_所構成的,這個后面會講到。單個reserved_semispace_size_在32位上reserved_semispace_size_(8 * Max(LUMP_OF_MEMORY, Page:: kPageSize)),由于Page:: kPageSize為1,所以為8MB,推算出64位的為16MB。因此,新生代內存的最大值在64位和32位上分別是32MB和16MB。

?? 對于老生代來說,max_old_generation_size_(700ul * LUMP_OF_MEMORY),32位為700MB,推算出64位的為1400MB。

?? 那堆內存的最大值是多少呢? ?? v8堆內存的最大保留空間可以從這個代碼中看出,其公式為:

// Returns the maximum amount of memory reserved for the heap. For // the young generation, we reserve 4 times the amount needed for a // semi space. The young generation consists of two semi spaces and // we reserve twice the amount needed for those in order to ensure // that new space can be aligned to its size intptr_t MaxReserved() {return 4 * reserved_semispace_size_ + max_old_generation_size_; } 復制代碼

?? 因此,在默認配置下V8堆內存最大值:

  • 32位:4*8+700=732MB;
  • 64位:4*16+1400=1464MB

2.2.5 V8內存算法

?? 在上面的提到了在內存分配的時候分為新生代和老生代。那新老生代之間有什么區別?如何分配的呢?

?? 接下來我們先講新生代的那些事兒:

2.2.5.1 新生代(Scavenge算法)

?? 新生代主要是存放存活時間較短的對象,這些對象主要是用Scavenge算法進行垃圾回收,在Scavenge的具體 實現中,主要采用了Cheney算法。

?? Cheney 算法是一種采用復制的方式實現的垃圾回收算法。它將堆內存一分為二,每一部分空間稱為 semispace。在這兩個 semispace 空間中,只有一個處于使用中,另一個處于閑置狀態。處于使用狀態的 semispace 空間稱為 From 空間,處于閑置狀態的空間稱為 To 空間。當我們分配對象時,先是在 From 空間中進行分配。當開始進行垃圾回收時,會檢查 From 空間中的存活對象,這 些存活對象將被復制到 To 空間中,而非存活對象占用的空間將會被釋放。完成復制后,From 空 間和To空間的角色發生對換。 簡而言之, 在垃圾回收的過程中, 就是通過將存活對象在兩個 semispace 空間之間進行復制。

?? Scavenge算法只能使用堆內存的一半。但是由于只復制存活的對象,并且對于生命周期短的場景存活對象只占少部分,所以它具有極高的時間效率。相當于可以理解為犧牲空間換取時間的算法。

??其實From空間和To空間進行角色交換的時候是需要進行判斷檢查的,在一定條件下,需要將存活周期長的對象移動到老生代中,完成對象的晉升。

??對象晉升的主要條件有兩個:

  • 對象是否經歷過Scavenge回收。
  • To空間是否超過25%的限制。

下圖是判斷流程:

2.2.5.2 老生代(Mark-Sweep & Mark-Compact)

?? 由于在老生代中存放對象占較大比重,若再繼續使用新生代的Scavenge算法會產生兩個問題:

  • 由于存活對象比較多,復制存活對象的效率將會降低。
  • 浪費一半的空間。

?? 因此在老生代中采用了Mark-Sweep和Mark-Compact相結合的方式進行垃圾的回收。

  • Mark-Sweep是標記清除的意思,它分為標記和清除兩個階段。Mark-Sweep 在標記階段遍歷堆中的所有對象,并標記活著的對象,在隨后的清除階段中,只清除沒有被標記的對象。可以看出,Scavenge 中只復制活著的對象,而 Mark-Sweep 只清理死亡對象。
  • Mark-Compact是對象在標記為死亡后,在整理的過程中,將活著的對象往一端移動,移動完成后,直接清理掉邊界外的內存。這是由于Mark-Sweep 在進行一次標記清除回收后,內存空間會出現不連續的狀態引起的,因為這種內存碎片會對后續的內存分配造成問題,很可能出現需要分配一個大對象的情況,這時所有的碎片空間都無法完成此次分配,就會提前觸發垃圾回收,而這次回收是不必要的。

?? 接下來我們看看3種垃圾回收算法的簡單對比:

回收算法Mark-SweepMark-CompactScavenge
速度中等最慢最快
空間開銷少(碎片)少(碎片)雙倍空間(無碎片)
是否移動對象

?? V8主要使用Mark-Sweep,在空間不足的情況下對從新生代中晉升過來的對象進行分配才使用Mark-Compact。

2.2.5.3 增量標記(Incremental Marking)

?? 在執行上述三種算法的時候,垃圾回收機制會先把應用邏輯暫停下來,待執行垃圾回收完后再恢復執行應用邏輯。“停頓”現在新老生代中都會發生,新生代由于存活對象時間短,全停頓對全局影響不大,但是在老生代中配置較大,且存活對象較多,全停頓的話影響比較大,因此需要改善。

?? 這時候就需要引入“增量標記”的方式,也就是拆分為許多小的“進步”,每做完一“進步”就讓JavaScript應用邏輯執行一會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。

?? 例如:一次執行標記可能需要幾百毫秒才能完成一個大的堆。

??在增量標記期間,垃圾收集器將標記工作分解為更小的塊,并且允許應用程序在塊之間運行:

??垃圾收集器選擇在每個塊中執行多少增量標記來匹配應用程序的分配速率。一般情況下,這極大地提高了應用程序的相應速度。對內存壓力較大的堆,收集器仍然可能出現長時間的暫停來維持分配。

??總的來說,V8經過增量標記后的,垃圾回收機制最大停頓時間可以減少到原本的1/6左右。同時還引入了延遲清理和增量式整理,讓清理與整理也變成增量式。

2.2.5.4 并行標記

??并行標記發生在主線程和工作線程上。應用程序在整個并行標記階段暫停。它是 stop-the-world 標記的多線程版本。

??并發標記主要發生在工作線程上。當并發標記正在進行時,應用程序可以繼續運行。

??在并行標記的時候,我們可以假定應用都不會同時運行。這大大的簡化了實現,是因為我們可以假定對象圖是靜態的,而且不會改變。為了并行標記對象圖,我們需要讓垃圾收集數據結構的線程是安全的,而且尋找一個可以在線程間運行的高效共享標記的方法。下面的示意圖展示了并行標記包含的數據結構。箭頭代表數據流的方向。簡單來說,示意圖省略了堆碎片處理所需的數據結構。

??注意,這些線程只能讀取對象圖,而不能修改它。對象的標記位和標記列表必須支持讀寫訪問。

2.2.5.4 并發標記

??并發標記允許 JavaScript 在主線程上運行,而工作線程正在訪問堆上的對象。這為潛在的競態數據打開大門。舉個例子:當工作者線程正在讀取字段時,JavaScript 可能正在寫入對象字段。競態數據會混淆垃圾回收器釋放活動對象或者將原始值和指針混合在一起。

主線程的每個改變對象圖表的操作將會是競態數據的潛在來源。由于 V8 是具有多種對象布局優化功能的高性能引擎,潛在競態數據來源目錄相當長。以下是高層次故障:

  • 對象分配

  • 寫對象

  • 對象布局變化

  • 快照反序列化

  • 功能脫優化實現

  • 年輕代垃圾回收期間的疏散

  • 代碼修補

??在以上這些操作上,主線程需要與工作線程同步。同步代價和復雜度是操作而定。大部分操作允許輕量級的同步和院子操作之間的訪問,但是少部分操作需獨占訪問對象。

??總的來說,并發標記就是為解決數據競爭的問題。

??有了平行標記與并發標記后,對比上面講的流程,GC的流程變為: 從root對象開始掃描,填充對象到marking worklist 分布并發標記任務到worker threads worker threads幫助main thread去更快地消費marking worklist中的對象 main thread 偶爾會通過執行bailout worklist 和 marking worklist來marking 一旦marking worklists為空,main thread 就完成GC行為 在結束之前,main thread重新掃描roots,可能會發現其他的白色節點,這些白色節點會在worker threads的幫助下,被平行標記。

課外學習

《深入簡出nodeJS》很不錯哦~

《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

總結

以上是生活随笔為你收集整理的Node的垃圾回收机制与内存溢出捕获(上)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。