教育行業(yè)A股IPO第一股(股票代碼 003032)

全國(guó)咨詢/投訴熱線:400-618-4000

深入理解Node.js中的垃圾回收和內(nèi)存泄漏的捕獲

更新時(shí)間:2015年12月29日14時(shí)54分 來源:傳智播客前端與移動(dòng)開發(fā)學(xué)科 瀏覽次數(shù):

深入理解Node.js中的垃圾回收和內(nèi)存泄漏的捕獲

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