深入理解Node.js中的垃圾回收和內(nèi)存泄漏的捕獲
原文鏈接
http://www.csdn.net/article/2015-11-24/2826316
對于Node.js而言,通常被抱怨最多的是它的性能問題。當(dāng)然這并不意味著Node.js在性能方面就比其他技術(shù)表現(xiàn)的都更 差, 因此開發(fā)者有必要清晰的理解Node.js是具體如何工作的的。由于這個技術(shù)有一個非常扁平的學(xué)習(xí)曲線, 如果要跟蹤Node.js的運行,通常都比較復(fù)雜,因此你需要提前理解它的運行機制,從而避免可能存在的性能損失。一旦出現(xiàn)了問題, 你需要盡快的定位它并進行修復(fù)。本文主要介紹了如何管理Node.js應(yīng)用的內(nèi)存,以及如何向下追蹤與內(nèi)存相關(guān)的問題。
Node.js內(nèi)存管理
不 同于PHP這樣的平臺,Node.js應(yīng)用是一個一直運行的進程。雖然這種機制有很多的優(yōu)點,例如在配置數(shù)據(jù)庫連接信息時, 只需要建立一次連接,便可以讓所有的請求進行復(fù)用該連接信息,但不幸的是,這種機制也存在缺陷。 但是,首先我們還是來了解一些Node.js基本知識。
Node.js是一個由JavaScript V8引擎控制的C++程序
Google V8是 一個由Google開發(fā)的JavaScript引擎,但它也可以脫離瀏覽器被單獨使用。 這使得它能夠完美的契合Node.js,實際上V8也是Node.js平臺中唯一能夠理解JavaScript的部分。 V8會將JavaScript代碼向下編譯為本地代碼(native code),然后執(zhí)行它。在執(zhí)行期間,V8會按需進行內(nèi)存的分配和釋放。 這意味著,如果我們在談?wù)揘ode.js的內(nèi)存管理問題,也就是在說V8的內(nèi)存管理問題。
你可以參考
這個鏈接來了解如何從C++的角度使用V8。
V8的內(nèi)存管理模式
一個運行的程序通常是通過在內(nèi)存中分配一部分空間來表示的。這部分空間被稱為駐留集(Resident Set)。 V8的內(nèi)存管理模式有點類似于
Java虛擬機(JVM),它會將內(nèi)存進行分段:
· 代碼 Code:實際被執(zhí)行的代碼
· 棧 Stack:包括所有的攜帶指針引用堆上對象的值類型(原始類型,例如整型和布爾),以及定義程序控制流的指針。
· 堆 Heap:用于保存引用類型(包括對象、字符串和閉包)的內(nèi)存段
在Node.js中,當(dāng)前的內(nèi)存使用情況可以輕松的使用
process.memoryUsage()進行查詢, 實例程序如下:
[js]
view plaincopy
1. var util = require('util');
2. console.log(util.inspect(process.memoryUsage()));
這將會在控制臺產(chǎn)生如下結(jié)果:
[js]
view plaincopy
1. {
2. rss: 4935680,
3. heapTotal: 1826816,
4. heapUsed: 650472
5. }
process.memoryUsage()函數(shù)返回的對象包含:
· 常駐集的大小 - rss
· 堆的總值 - heapTotal
· 實際使用的堆 - heapUsed
我們可以利用這個函數(shù)來記錄不同時間的內(nèi)存使用情況,并利用這些數(shù)據(jù)繪制成一張圖從而更清晰的展示V8是如何處理內(nèi)存的。
圖 中最頂端的橙色線條為RSS(駐留集大小),接下來紅色線條表示堆的總值,表現(xiàn)的最為不穩(wěn)定的部分是黃色線條, 它所表示的是已使用的堆的大小,雖然線條不停的抖動,但總是維持在一定的邊界值內(nèi)保持一個穩(wěn)定中位數(shù)。 分配和回收堆內(nèi)存的機被稱為垃圾回收(Garbage Collection)。
垃圾回收
每個需要消耗內(nèi)存的程序都需要 某種機制來預(yù)約和釋放內(nèi)存空間。在C和C++程序中,程序可以通過malloc()和free() 這兩個函數(shù)來申請和釋放內(nèi)存。我們發(fā)現(xiàn),這需要由程序員負責(zé)釋放不再使用的堆內(nèi)存空間。如果一個程序所分配的內(nèi)存不再使用了, 卻沒有被及時釋放的話,那么逐漸累積會導(dǎo)致程序?qū)Χ芽臻g的消耗越來越大,直至耗盡整個堆空間,此時會導(dǎo)致程序崩潰。 通常我們稱這種情況為內(nèi)存泄漏(memory leak)。
前面我們已經(jīng)了解到,Node.js的JavaScript代碼會通過V8編譯 為本地代碼(Native Code)。 顯然最終的原始數(shù)據(jù)結(jié)構(gòu)已經(jīng)和最初的表示沒有太多的關(guān)系了,它完全由V8來進行管理。這說明, 在JavaScript中,我們并不能主動的進行內(nèi)存的分配和回收操作。V8使用了著名的被稱為“垃圾回收”的機制來自動解決這個問題。
垃圾回收背后的理論非常的簡單:如果內(nèi)存段不再被其他地方引用,我們便可以假設(shè)它已經(jīng)不再被使用,因此,就可以釋放這片內(nèi)存段。 然而, 檢索和維護這些信息是非常復(fù)雜的,因為這可能會涉及到引用之間的相互鏈接,從而形成一個復(fù)雜的圖結(jié)構(gòu)。
在上面的堆圖中,如果紅色的對象不再有引用指向它的話,那么該對象就可以被丟棄(釋放內(nèi)存)。
垃圾回收是個代價非常高的進程,因為它會中斷程序在執(zhí)行,從而影響程序的性能。為了補救這種情況,V8使用了兩種類型的垃圾回收:
· Scavenge(提取),速度快但不徹底
· Mark-Sweep(標記-清除),相對慢一點,但是可以回收所有未被引用的內(nèi)存
你可以通過
這篇博文深入的了解更多關(guān)于V8垃圾回收的內(nèi)容。
重新回顧我們利用process.memoryUsage()方法收集到的數(shù)據(jù),我們可以很簡單的就識別出不同的垃圾回收類型: 成鋸齒狀(saw-tooth pattern)是由Scavenge創(chuàng)建的,而出現(xiàn)向下跳躍的則是由Mark-Sweep操作產(chǎn)生的。
通過使用原生模塊
node-gc-profiler,我們可以收集更多關(guān)于垃圾回收的信息。 該模塊會訂閱由V8觸發(fā)的所有垃圾回收事件,并將它們暴露給JavaScript。
返回的對象表示了垃圾回收的類型和持續(xù)時間。再一次的,我們可以輕松的利用可視化圖形來更好的理解它是如何工作的。
我們可以發(fā)現(xiàn)Scavenge Compact運行的比Mark Sweep更為頻繁。根據(jù)應(yīng)用的復(fù)雜程度這可能會存在一定的變化。 有意思的是,上面的圖形也展現(xiàn)了頻繁卻非常短的Mark-Sweep運行狀況,這也跟運行的函數(shù)有關(guān)。
如果出了故障
既然有垃圾回收器來負責(zé)內(nèi)存清理,那么為什么我們還需要關(guān)心這個呢?事實上,這仍然會有可能發(fā)生內(nèi)存泄漏, 你的日志記錄可能會記錄這些信息。
當(dāng)內(nèi)存泄漏出現(xiàn)的時候,內(nèi)存可能會出現(xiàn)堆積的情況,如圖所示。
垃 圾回收(GC)機制會盡可能的回收內(nèi)存,但是每次運行GC都會導(dǎo)致一定的損耗。我們發(fā)現(xiàn)在上圖中,堆內(nèi)存的使用處于一個不斷攀升的過程, 這通常意味著內(nèi)存泄漏的發(fā)生。使用這些信息,我們能夠較為方便的判斷是否出現(xiàn)了內(nèi)存泄漏, 下面我們進一步的探索如何在內(nèi)存泄漏發(fā)生的是去向下最終問題的源頭。
問題追蹤和解決
有些泄漏的發(fā)生是顯而易見的,例如將數(shù)據(jù)存儲在全局變量中,例如將每次訪問用戶的IP信息都存放在一個數(shù)組中。 而有些問題則是不易察覺的,例如著名的
沃爾瑪內(nèi)存泄漏事件, 它是由于Node.js核心代碼中一個非常細微的聲明缺失導(dǎo)致的,這可能需要花費數(shù)周的事件才能追蹤到。
在這里我并不會覆蓋核心的代碼錯誤。而是來看一個難以追蹤的內(nèi)存泄漏案例,通過這個例子能夠讓你在自己的JavaScript代碼中定位錯誤, 這個例子來源于
Meteor的博客。
這 段代碼剛看到的時候并沒有發(fā)現(xiàn)有什么問題。我們可以認為theTing在每次調(diào)用replaceThing()的時候都會被覆寫。 問題就是someMethod擁有作為上下文的封閉作用域。這意味著unused()是在someMethod()內(nèi)部的,甚至unused()從未被調(diào) 用過, 這也就以為了垃圾收集器無法釋放originalThing。有非常多的間接方法需要遵守。這在代碼中并非是bug,但它會導(dǎo)致內(nèi)存泄漏, 并且難以追蹤。
因此如果我們能夠進入堆內(nèi)存,并且觀察它實際包含的內(nèi)容,這會非常有助于我們最終錯誤源。幸運的是,我們可以這么做! V8提供了一種方法用于轉(zhuǎn)儲(導(dǎo)出)當(dāng)前的堆,并且v8-profiler將它用JavaScript接口的形式暴露了出來。
如 果內(nèi)存使用持續(xù)攀升的話,這個簡單的模塊可以創(chuàng)建了堆的轉(zhuǎn)儲文件。當(dāng)然,也有其他更巧妙的方法來探測類似的問題, 但對于我們的當(dāng)前任務(wù)而言,這就足夠了。如果存在內(nèi)存泄漏,程序會中斷,并且伴隨著大量的類似文件。 因此你可以通過為這個模塊關(guān)閉和增加一些提示工具的方式來模擬。在Chrome中也提供了類似的堆空間轉(zhuǎn)儲功能, 并且你可以直接通過Chrome開發(fā)者工具來分析v8-profiler的轉(zhuǎn)儲文件。
單一的堆轉(zhuǎn)儲可能并不能幫助你,因為它不能展示堆隨著時間變化的增長過程。這就是為什么Chrome開發(fā)者工具允許你對比不同的內(nèi)存概況文件。 你可以通過比較兩個專注文件來獲得差值,這樣可以讓你觀察到內(nèi)存占用的變化情況。如下圖所示:
這 里能夠看到一些問題所在,longStr變量包含著一些星號組成的字符串,并且被originalThing所引用,并且也被一些方法所引用, 然后也被……當(dāng)然,你能看意識到這點。這里有一個非常長的引用路徑,閉包上下文會導(dǎo)致longStr長期占用內(nèi)存,并且得不到釋放。
雖然這個問題導(dǎo)致了一個顯而易見的問題,但是定位問題的過程總是相似的:
1. 不定時的創(chuàng)建堆的轉(zhuǎn)儲文件
2. 進行不同文件的對比,從而定位問題所在
總結(jié)
正如我們所看到的,垃圾收集是個非常復(fù)雜的過程,并且即使代碼沒有問題也有可能會導(dǎo)致內(nèi)存泄漏。 通過使用v8(和chrome開發(fā)者工具)提供的一些開箱即用的功能,能夠幫助我們定位問題的源頭, 如果你將這種機制構(gòu)建到你的應(yīng)用內(nèi),這將會非常有助于你發(fā)現(xiàn)和修復(fù)問題。
當(dāng)然,如果你問我上面的代碼如何修復(fù),其實非常的簡單,只要在函數(shù)的最后加上一行theThing = null;即可。