歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 深入理解Node.js中的垃圾回收和內存洩漏的捕獲

深入理解Node.js中的垃圾回收和內存洩漏的捕獲

日期:2017/3/1 9:19:11   编辑:Linux編程

對於Node.js而言,通常被抱怨最多的是它的性能問題。當然這並不意味著Node.js在性能方面就比其他技術表現的都更差, 因此開發者有必要清晰的理解Node.js是具體如何工作的的。由於這個技術有一個非常扁平的學習曲線, 如果要跟蹤Node.js的運行,通常都比較復雜,因此你需要提前理解它的運行機制,從而避免可能存在的性能損失。一旦出現了問題, 你需要盡快的定位它並進行修復。本文主要介紹了如何管理Node.js應用的內存,以及如何向下追蹤與內存相關的問題。

Node.js內存管理

不同於PHP這樣的平台,Node.js應用是一個一直運行的進程。雖然這種機制有很多的優點,例如在配置數據庫連接信息時, 只需要建立一次連接,便可以讓所有的請求進行復用該連接信息,但不幸的是,這種機制也存在缺陷。 但是,首先我們還是來了解一些Node.js基本知識。

Node.js是一個由JavaScript V8引擎控制的C++程序

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

你可以參考這個鏈接來了解如何從C++的角度使用V8。

V8的內存管理模式

一個運行的程序通常是通過在內存中分配一部分空間來表示的。這部分空間被稱為駐留集(Resident Set)。 V8的內存管理模式有點類似於Java虛擬機(JVM),它會將內存進行分段:

  • 代碼 Code:實際被執行的代碼
  • 棧 Stack:包括所有的攜帶指針引用堆上對象的值類型(原始類型,例如整型和布爾),以及定義程序控制流的指針。
  • 堆 Heap:用於保存引用類型(包括對象、字符串和閉包)的內存段

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

var util = require('util');

console.log(util.inspect(process.memoryUsage()));

這將會在控制台產生如下結果:

{ 
    rss: 4935680,
    heapTotal: 1826816,
    heapUsed: 650472
}

process.memoryUsage()函數返回的對象包含:

  • 常駐集的大小 - rss
  • 堆的總值 - heapTotal
  • 實際使用的堆 - heapUsed

我們可以利用這個函數來記錄不同時間的內存使用情況,並利用這些數據繪制成一張圖從而更清晰的展示V8是如何處理內存的。

圖中最頂端的橙色線條為RSS(駐留集大小),接下來紅色線條表示堆的總值,表現的最為不穩定的部分是黃色線條, 它所表示的是已使用的堆的大小,雖然線條不停的抖動,但總是維持在一定的邊界值內保持一個穩定中位數。 分配和回收堆內存的機被稱為垃圾回收(Garbage Collection)。

垃圾回收

每個需要消耗內存的程序都需要某種機制來預約和釋放內存空間。在C和C++程序中,程序可以通過malloc()free() 這兩個函數來申請和釋放內存。我們發現,這需要由程序員負責釋放不再使用的堆內存空間。如果一個程序所分配的內存不再使用了, 卻沒有被及時釋放的話,那麼逐漸累積會導致程序對堆空間的消耗越來越大,直至耗盡整個堆空間,此時會導致程序崩潰。 通常我們稱這種情況為內存洩漏(memory leak)。

前面我們已經了解到,Node.js的JavaScript代碼會通過V8編譯為本地代碼(Native Code)。 顯然最終的原始數據結構已經和最初的表示沒有太多的關系了,它完全由V8來進行管理。這說明, 在JavaScript中,我們並不能主動的進行內存的分配和回收操作。V8使用了著名的被稱為“垃圾回收”的機制來自動解決這個問題。

垃圾回收背後的理論非常的簡單:如果內存段不再被其他地方引用,我們便可以假設它已經不再被使用,因此,就可以釋放這片內存段。 然而, 檢索和維護這些信息是非常復雜的,因為這可能會涉及到引用之間的相互鏈接,從而形成一個復雜的圖結構。

在上面的堆圖中,如果紅色的對象不再有引用指向它的話,那麼該對象就可以被丟棄(釋放內存)。

垃圾回收是個代價非常高的進程,因為它會中斷程序在執行,從而影響程序的性能。為了補救這種情況,V8使用了兩種類型的垃圾回收:

  • Scavenge(提取),速度快但不徹底
  • Mark-Sweep(標記-清除),相對慢一點,但是可以回收所有未被引用的內存

你可以通過這篇博文深入的了解更多關於V8垃圾回收的內容。

重新回顧我們利用process.memoryUsage()方法收集到的數據,我們可以很簡單的就識別出不同的垃圾回收類型: 成鋸齒狀(saw-tooth pattern)是由Scavenge創建的,而出現向下跳躍的則是由Mark-Sweep操作產生的。

通過使用原生模塊node-gc-profiler,我們可以收集更多關於垃圾回收的信息。 該模塊會訂閱由V8觸發的所有垃圾回收事件,並將它們暴露給JavaScript。

返回的對象表示了垃圾回收的類型和持續時間。再一次的,我們可以輕松的利用可視化圖形來更好的理解它是如何工作的。

我們可以發現Scavenge Compact運行的比Mark Sweep更為頻繁。根據應用的復雜程度這可能會存在一定的變化。 有意思的是,上面的圖形也展現了頻繁卻非常短的Mark-Sweep運行狀況,這也跟運行的函數有關。

如果出了故障

既然有垃圾回收器來負責內存清理,那麼為什麼我們還需要關心這個呢?事實上,這仍然會有可能發生內存洩漏, 你的日志記錄可能會記錄這些信息。

當內存洩漏出現的時候,內存可能會出現堆積的情況,如圖所示。

垃圾回收(GC)機制會盡可能的回收內存,但是每次運行GC都會導致一定的損耗。我們發現在上圖中,堆內存的使用處於一個不斷攀升的過程, 這通常意味著內存洩漏的發生。使用這些信息,我們能夠較為方便的判斷是否出現了內存洩漏, 下面我們進一步的探索如何在內存洩漏發生的是去向下最終問題的源頭。

問題追蹤和解決

有些洩漏的發生是顯而易見的,例如將數據存儲在全局變量中,例如將每次訪問用戶的IP信息都存放在一個數組中。 而有些問題則是不易察覺的,例如著名的沃爾瑪內存洩漏事件, 它是由於Node.js核心代碼中一個非常細微的聲明缺失導致的,這可能需要花費數周的事件才能追蹤到。

在這裡我並不會覆蓋核心的代碼錯誤。而是來看一個難以追蹤的內存洩漏案例,通過這個例子能夠讓你在自己的JavaScript代碼中定位錯誤, 這個例子來源於Meteor的博客。

這段代碼剛看到的時候並沒有發現有什麼問題。我們可以認為theTing在每次調用replaceThing()的時候都會被覆寫。 問題就是someMethod擁有作為上下文的封閉作用域。這意味著unused()是在someMethod()內部的,甚至unused()從未被調用過, 這也就以為了垃圾收集器無法釋放originalThing。有非常多的間接方法需要遵守。這在代碼中並非是bug,但它會導致內存洩漏, 並且難以追蹤。

因此如果我們能夠進入堆內存,並且觀察它實際包含的內容,這會非常有助於我們最終錯誤源。幸運的是,我們可以這麼做! V8提供了一種方法用於轉儲(導出)當前的堆,並且v8-profiler將它用JavaScript接口的形式暴露了出來。

如果內存使用持續攀升的話,這個簡單的模塊可以創建了堆的轉儲文件。當然,也有其他更巧妙的方法來探測類似的問題, 但對於我們的當前任務而言,這就足夠了。如果存在內存洩漏,程序會中斷,並且伴隨著大量的類似文件。 因此你可以通過為這個模塊關閉和增加一些提示工具的方式來模擬。在Chrome中也提供了類似的堆空間轉儲功能, 並且你可以直接通過Chrome開發者工具來分析v8-profiler的轉儲文件。

單一的堆轉儲可能並不能幫助你,因為它不能展示堆隨著時間變化的增長過程。這就是為什麼Chrome開發者工具允許你對比不同的內存概況文件。 你可以通過比較兩個專注文件來獲得差值,這樣可以讓你觀察到內存占用的變化情況。如下圖所示:

這裡能夠看到一些問題所在,longStr變量包含著一些星號組成的字符串,並且被originalThing所引用,並且也被一些方法所引用, 然後也被……當然,你能看意識到這點。這裡有一個非常長的引用路徑,閉包上下文會導致longStr長期占用內存,並且得不到釋放。

雖然這個問題導致了一個顯而易見的問題,但是定位問題的過程總是相似的:

  1. 不定時的創建堆的轉儲文件
  2. 進行��同文件的對比,從而定位問題所在

總結

正如我們所看到的,垃圾收集是個非常復雜的過程,並且即使代碼沒有問題也有可能會導致內存洩漏。 通過使用v8(和chrome開發者工具)提供的一些開箱即用的功能,能夠幫助我們定位問題的源頭, 如果你將這種機制構建到你的應用內,這將會非常有助於你發現和修復問題。

當然,如果你問我上面的代碼如何修復,其實非常的簡單,只要在函數的最後加上一行theThing = null;即可。

下面關於Node.js的內容你可能也喜歡

在 Ubuntu 14.04/15.04 上安裝配置 Node.js v4.0.0 http://www.linuxidc.com/Linux/2015-10/123951.htm

如何在CentOS 7安裝Node.js http://www.linuxidc.com/Linux/2015-02/113554.htm

Ubuntu 14.04下搭建Node.js開發環境 http://www.linuxidc.com/Linux/2014-12/110983.htm

Ubunru 12.04 下Node.js開發環境的安裝配置 http://www.linuxidc.com/Linux/2014-05/101418.htm

Node.Js入門[PDF+相關代碼] http://www.linuxidc.com/Linux/2013-06/85462.htm

Node.js開發指南 高清PDF中文版 +源碼 http://www.linuxidc.com/Linux/2014-09/106494.htm

Node.js入門開發指南中文版 http://www.linuxidc.com/Linux/2012-11/73363.htm

Ubuntu 編譯安裝Node.js http://www.linuxidc.com/Linux/2013-10/91321.htm

Node.js 的詳細介紹:請點這裡
Node.js 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved