翻譯:瘋狂的技術宅
作者:Giovanny Gongora
來源:nodesource
正文共:3955?字
預計閱讀時間:10分鐘
一直以來,跟蹤 Node.js 的內存泄漏是一個反復出現的話題,人們始終希望對其復雜性和原因了解更多。
并非所有的內存泄漏都顯而易見。但是,一旦我們確定了其模式,就必須在內存使用率,內存中保存的對象和響應時間之間尋找關聯。在檢查對象時,應該根據自己所用的框架或技術(例如服務器端渲染),研究收集了多少對象,以及它們是否正常。希望在完成本文結束之后,你將能夠理解并尋找一種策略來調試 Node.js 程序的內存消耗。
Node.js 中的垃圾回收機制
JavaScript 是一種垃圾回收語言,而 Google 的 V8 最初是為 Google Chrome 創建的JavaScript引擎,在許多情況下都可以用作獨立的運行時。Node.js 中垃圾收集器的兩個重要操作是:
確定有用的或無用的對象,并且
回收或重用無用對象所占用的內存。
需要記住的要點:在垃圾回收器運行時,它將完全暫停你的程序,直到完成工作為止。因此,你需要通過維護對象的引用來最大程度地減少其工作。
V8 JavaScript 引擎會自動分配和取消分配 Node.js 進程使用的所有內存。讓我們看看實際情況是怎樣的。
如果你將內存視為一個樹結構,那么可以想象 V8 從“根節點”開始保存程序中所有的變量。這可能是你的 window 對象,也可能是 Node.js 模塊中的全局對象,通常稱為控制者。需要牢記的一點是,你無法對怎樣取消分配“根”節點進行控制。
接下來,你將找到一個 Object 節點,通常被稱為葉子(沒有子引用的節點)。最后 JavaScript 中有 4 種數據類型:布爾值,字符串,數字和對象。
V8 將遍歷該樹并嘗試識別無法從“根”節點訪問的數據組。如果無法從“根”節點訪問該數據,則 V8 假定不再使用該數據,并釋放內存。請記住:要確定某個對象是否處于活動狀態,需要檢查是否可通過被定義為活動對象的某個指針鏈到達;其他所有的情況,例如無法從根節點訪問,或無法被根節點或另一個活動對象引用的對象,都會被視為垃圾。
簡而言之,垃圾收集器有兩個主要任務:
跟蹤
計算對象之間的引用。
當你需要跟蹤來自另一個進程的遠程引用時,它可能會變得很棘手,但是在 Node.js 程序中,我們通常用單進程,這樣使我們更加輕松。
V8 的內存方案
V8 使用類似于 Java 虛擬機的方案,并將內存劃分為多個段。實現這種包裝方案的東西被稱為“駐留集”,它是指在 RAM 中駐留的進程所占用的內存部分。
在駐留集中,你會發現:
代碼段:代碼實際執行的位置。
棧: 包含局部變量和所有值類型,其指針引用堆上的對象或定義程序的控制流。
堆: 專門用于存儲引用類型(如對象、字符串和閉包)的內存段。
還有重要的兩點要記住:
對象的淺大小:保存對象本身所需的內存大小
對象的保留大小:當刪除對象及其依賴對象時,被釋放的內存大小
Node.js 有一個對象,以字節為單位描述 Node.js 進程的內存使用情況。在對象內部,你會發現:
rss: 是指駐留集大小。
heapTotal 和 heapUsed: 是指 V8 的內存使用情況。
external: 是指與 V8 所管理的 JavaScript 對象綁定的 C++ 對象的內存使用情況。
查找泄漏
Chrome DevTools 是一個很棒的工具,可用于通過遠程調試來診斷 Node.js 程序中的內存泄漏。也有其他為你提供類似功能的工具。但是,你需要記住,概要分析是一項繁重的 CPU 任務,可能會對你的程序產生負面影響,一定要注意這一點!
我們將要介紹的 Node.js 程序是一個簡單的 HTTP API Server,它具有多個端點,向使用該服務的人返回不同的信息。你可以克隆這個程序的repository。
1const?http?=?require('http')
2
3const?leak?=?[]
4
5function?requestListener(req,?res)?{
6
7??if?(req.url?===?'/now')?{
8????let?resp?=?JSON.stringify({?now:?new?Date()?})
9????leak.push(JSON.parse(resp))
10????res.writeHead(200,?{?'Content-Type':?'application/json'?})
11????res.write(resp)
12????res.end()
13??}?else?if?(req.url?===?'/getSushi')?{
14????function?importantMath()?{
15??????let?endTime?=?Date.now()?+?(5?*?1000);
16??????while?(Date.now()?17????????Math.random();
18??????}
19????}
20
21????function?theSushiTable()?{
22??????return?new?Promise(resolve?=>?{
23????????resolve('?');
24??????});
25????}
26
27????async?function?getSushi()?{
28??????let?sushi?=?await?theSushiTable();
29??????res.writeHead(200,?{?'Content-Type':?'text/html;?charset=utf-8'?})
30??????res.write(`Enjoy!?${sushi}`);
31??????res.end()
32????}
33
34????getSushi()
35????importantMath()
36??}?else?{
37????res.end('Invalid?request')
38??}
39}
40
41const?server?=?http.createServer(requestListener)
42server.listen(process.env.PORT?||?3000)
啟動Node.js應用程序:
我們一直在使用 3S(3 Snapshot)方法進行診斷并確定可能的內存問題。有趣的是,我們發現這是 Gmail 團隊的 Loreena Lee 長期使用的一種解決內存問題的方法。此方法的步驟:
打開 Chrome DevTools 并訪問
chrome://inspect
。在底部的“Remote Target”中,單擊
inspect
按鈕。
注意: 要確保已將 Inspector 附加到要分析的 Node.js 程序。你還可以用 ndb
連接到 Chrome DevTools。
當應用運行時,你將在控制臺的輸出中看到一條 Debugger Connected
消息。
轉到 Chrome DevTools > Memory
獲取堆快照
在這種情況下,我們得到了第一個快照,而服務沒有進行任何負載或處理。這是針對某些用例的提示:如果我們能夠確定在接受請求或進行某些處理之前不需要對程序進行任何預熱,那就很好了。有時,在獲取第一個堆快照之前先進行熱身操作是有意義的,因為在某些情況下,你可能會在第一次調用時對全局變量進行了延遲初始化。
在你的程序中執行你認為導致內存泄漏的操作。
在這種情況下,我們將運行 ?npm run load-mem
。這將啟動 ab
來模擬 Node.js 應用程序中的流量或負載。
得到堆快照
再次在你的程序中執行你認為會導致內存泄漏的操作。
獲取最終的堆快照
選擇最新得到的快照。
在窗口頂部,找到顯示 “All objects” 的下拉列表,并將其切換為“Objects allocated between snapshots 1 and 2”。(如果需要,你也可以對 2 和 3 執行相同的操作)。這將大大減少你看到的對象數量。
比較視圖也可以幫你識別那些對象:
在該視圖中,你將看到泄漏對象的列表:頂級條目(每個構造函數一行)、對象到GC根的距離、對象實例數、淺大小和保留大小。你可以通過選擇一行來查看其內容。一個好的經驗法則是,首先忽略括號中的項目,因為它們是內置結構。@
字符是對象的唯一 ID,可讓你比較每個對象的堆快照。
典型的內存泄漏可能是通過意外地將對對象的引用存儲在無法進行垃圾回收的全局對象中,從而保留了預期僅在一個請求周期內持續存在的對象的引用。
這個例子故意留下了一個內存泄漏的問題,在請求一個從 API 查詢返回的對象時生成帶有日期時間戳的隨機對象,并將其存儲在全局數組中來泄漏該對象。通過查看幾個保留的對象,你會看到一些泄漏數據的示例,可用于跟蹤應用程序中的泄漏。
NSolid 非常適合這種類型的用例,因為它可以使你很好地了解在執行的每個任務或負載測試中內存是怎樣增加的。如果你感到好奇,還可以實時查看每個性能分析動作如何影響 CPU。
在實際項目中,你不可能總是盯著用于監視程序的工具。NSolid 的一大優點是可以為應用程序的不同指標設置閾值和限制。例如,你可以將 NSolid 設置為在使用的內存量超過 X 時,或者在 X 時間內尚未從高消耗高峰恢復內存的情況下,進行堆快照。聽起來不錯吧?
標記和清理
V8 的垃圾收集器主要基于 Mark-Sweep 收集算法,該算法包括跟蹤垃圾收集,該操作通過標記可達的對象,然后清理內存并回收未標記的對象(必須無法訪問),將其納入釋放列表。這也稱為世代垃圾收集器,對象可以在新聲代、從新生代到老生代、以及老生代中移動。
移動對象的代價非常打,因為需要將對象的基礎內存復制到新位置,并且指向這些對象的指針也需要更新。
用人話解釋:
V8 遞歸查找所有對象到“根”節點的引用路徑。例如:在 JavaScript 中,“window” 對象是可以充當 Root 的全局變量的示例。window 對象始終存在,因此垃圾收集器可以認為它及其所有子對象始終存在(即不是垃圾)。如果有任何引用,則沒有指向“根”節點的路徑。特別是當它以遞歸方式查找未引用的對象時,將被標記為垃圾,稍后將會被清除以釋放該內存并將其返回給操作系統。
但是,現代的垃圾收集器以不同的方式對這種算法進行了改進,但本質是相同的:可訪問的內存被標記為一類,其余的被視為垃圾。
請記住,從根可以訪問到的所有內容均不視為垃圾。不需要的引用是保留在代碼中某個位置的變量,這些變量將不再使用,并且指向可以釋放的內存,因此,要了解 JavaScript 中最常見的泄漏,我們需要了解通常忘記引用的方式。
Orinoco 垃圾收集器
Orinoco 是最新 GC 項目的代號,它利用最新的增量和并發技術進行垃圾回收,并有釋放主線程的功能。描述 Orinoco 性能的重要指標之一是垃圾回收器執行時主線程暫停的頻率和時間。對于經典的“世界末日”收集者而言,這些時間間隔會因為延遲、質量差的渲染以及響應時間的增加而影響程序的用戶體驗。
V8 在新聲代內存中的輔助流之間分配垃圾回收工作(清除)。每個流接收一組指針,然后將所有活動對象移動到“to-space”。
將對象移至“to-space”時,線程需要通過讀、寫、比較和交換的原子操作進行同步,以避免出現另一個線程找到相同的對象但遵循不同路徑并嘗試移動的情況。
引用自 V8 官網:
在現有 GC 中添加并行、增量和并發技術是一項多年的努力,但已取得了回報,將大量工作移交給了后臺任務。它大大改善了暫停時間、延遲和頁面加載,使動畫、滾動和用戶交互更加順暢。并行的 Scavenger 根據工作量將主線程新聲代垃圾收集的總時間減少了大約 20%–50%。Idle-time GC 可以在 Gmail 空閑時將其 JavaScript 堆內存減少 45%。并發標記和清除可以將笨重的 WebGL 游戲中的暫停時間減少多達 50%。
Mark-Evacuate 收集器包括三個階段:標記、復制和更新指針。為了避免在新聲代中清理頁面以維護空閑列表,仍然使用 semi-space 來維護新生代,它始終保持緊湊狀態,即在垃圾回收期間將活動對象復制到 “to-space” 中。并行進行的好處是可以獲得“exact liveness”信息。通過僅移動和重新鏈接主要包含活動對象的頁面,可以用此信息來避免復制,這也可以由完整的 Mark-Sweep-Compact 收集器執行。它通過和標記清除算法相同的方式標記堆中的活動對象來工作,這意味著堆通常會被碎片化。V8 當前隨附有并行的 Scavenger,可在大量基準測試中減少主線程新生代垃圾回收約 20%–50% 的總時間。
與暫停主線程、響應時間和頁面加載有關的所有方面都得到了顯著改善,這使得頁面上的動畫、滾動和用戶交互更加流暢。并行收集器可以將新內存的總處理時間減少 20–50%,具體取決于負載。但是工作還沒有結束:減少停頓仍然是一項重要任務,我們將繼續尋找使用更先進的技術來實現這一目標的可能性。
總結
大多數開發人員在開發 JavaScript 程序時無需考慮 GC,但是了解一些內部知識可以幫助你考慮內存使用情況和有用的編程模式。例如考慮到 V8 中基于世代的堆結構,從 GC 角度來說,維護低生存期的對象的成本實際上是相當低的,因為我們主要為存在的對象付出代價。這種模式不僅特定于 JavaScript,而且對于許多支持垃圾回收的語言也都有效。
要點:
請勿使用過時或不推薦的軟件包(例如,node-memwatch,node-inspector 或 v8-profiler)來檢查內存。你需要的一切都已經集成在了 Node.js 的二進制文件中(尤其是 node.js 檢查器和調試器)。如果你需要更專業的工具,則可以使用 NSolid、Chrome DevTools 或其他知名軟件。
考慮在何時何地觸發堆快照和 CPU profile。由于要在生產環境中進行快照,你將會希望同時觸發這兩者(主要是在測試中),所以這會需要大量的 CPU 操作。另外,在關閉進程和進行冷重啟之前,請確認有多少堆轉儲被寫入了。
沒有哪一種工具可以解決所有問題。要根據程序的具體情況進行測試、測量、判斷和解決。選擇適合你體系結構的最佳工具,并選擇一種可以提供更多有用數據來幫你解決問題的工具。
原文:https://nodesource.com/blog/memory-leaks-demystified
?
2020年京程一燈全新課程體系即將推出,請保持關注。
愿你在新的一年里保持技術領先,有個好前程,愿你月薪30K。我們是認真的 !
?往期精彩回顧
面向開發人員的十大 NodeJS 框架
JavaScript 類完整指南
講給前端的正則表達式
WebAssembly 正式成為 Web 的第四種語言
2020 年 Node.js 將會有哪些新功能
2020 年 Web 開發展望
從 JavaScript、ES6、ES7 到 ES10,你學到哪兒了?
15個 Vue.js 高級面試題