nodejs內存控制


  • v8的內存限制
  • v8的垃圾回收機制
  • 高效使用內存與內存指標
  • 內存泄漏與內存泄漏排查
  • 大內存應用

 一、v8的內存限制

1.1為什么要關注內存?

在JavaScript中,它與Java一樣都是由垃圾回收機制來進行自動內存管理,這使得開發者不需要像C/C++開發那樣時刻關注內存的分配和釋放問題。所以在開發瀏覽器的前端頁面時,我們基本不關心內存的管理問題,這種不關心不代表問題不存在,一方面時JavaScript內核的內存管理機制基本能應付大部分的內存管理問題,另一方面是在客戶端沒有像服務上那樣對性能機制最求的需求,瀏覽器內核的垃圾回收機制有足夠的時間去實現內存回收操作。但這並不代表絕對的安全,在操作不慎的情況依然會有出現內存溢出的可能,關注內存管理或許不是在前端頁面開發中的首要任務,但也不是可以忽略的問題。

這一節的內容顯然不是圍繞前端開發展開的,在nodejs項目開發中對於內存管理的要求是要明顯區別於瀏覽器端的頁面開發。在使用nodejs開發對性能敏感的服務器端程序時,內存管理的好壞、垃圾回收狀況是否優良,都會對服務夠成影響。

v8以事件驅動、非阻塞I/O和其優秀的性能表現被Ryan Dahl選擇作為nodejs的JavaScript腳本引擎,但同樣也受到v8的限制,特別是這里我們需要討論的內存管理問題。

1.2v8的內存限制

nodejs中通過JavaScript使用內存時只能使用部分內存,(64位系統下約為1.4GB;32位系統下約為0.7GB)這導致即使物理內存可能有32GB,你也無法將2GB的文件讀如內存。造成這一問題的原因就是使用JavaScript對象基本上都是通過v8自己的方式分配和管理的,這在瀏覽器應用場景下綽綽有余,但在nodejs中這卻限制了開發者隨心所欲的使用內存的想法。即使大部分情況下服務端也不會經常有使用大內存的場景,但這就如同帶着鐐銬跳舞,如果實際中不小心碰觸到這個界限,會造成進程退出

要了解v8內存的使用量及為何限制內存,就需要回到v8在內存使用策略上,知道其原理才能避免問題並更好的管理內存。

1.3v8的對象分配

在v8中,所有JavaScript對象都是通過堆來進行分配的,nodejs中提供了v8中內存使用量的查看方式:

console.log( process.memoryUsage());
{
    rss: 18907136,       //常駐內存
    heapTotal: 4014080,  //當前v8申請使用的總的(堆)內存大小
    heapUsed: 2286912,   //當前腳本實際使用的內存大小
    external: 801290,    //擴展的內存大小
    arrayBuffers: 9382   //獨立的空間大小(不占用v8申請使用的內存大小)
}

v8的堆示意圖:

 

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

比如上面的測試代碼中打印的已申請的內存(heapTotal)為:4014080 / 1024 / 1024 = 3.83MB,隨着heapUsed的實際使用值逐漸增長至3.83時v8就會繼續申請內存,直到申請到1.4GB(假設系統為64位)並被消費完時就會導致進程退出。

從表面上看限制堆的大小是因為v8最初為瀏覽器的頁面設計的JavaScript引擎,對於網頁來說v8限制的堆大小綽綽有余,而實際上卻是v8的垃圾回收機制的限制。按照官方的說法以1.5GB堆內存垃圾回收為例,v8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收需要1秒以上。

在垃圾回收時會導致JavaScript線程暫停執行,這種由垃圾回收導致的性能直線式下降對於瀏覽器頁面來說都是一個不能接受的性能損耗,更別說nodejs作為后端服務,所以直接限制堆內存是最好的選擇。當然這種限制也不是不能打開,v8提供了兩個解除限制的啟動標志:--max-old-space-size 、--max-new-space-size來調整內存限制的大小:

node --max-old-space-size=1700 test.js // 單位MB(老生代堆內存最大空間)
node --max-new-space-size=1024 test.js //單位KB(新生代堆內存最大空間)

注意這種手動設置堆內存限制大小只在v8初識化時生效,一旦生效就不能再動態改變。關於老生代和新生代內存在2.1中會有介紹。

 二、v8的垃圾回收機制

 2.1v8的垃圾回收策略主要基於分代式垃圾回收機制:

在實際的應用中,對象的生命周期長短不一,不同的垃圾回收算法只能針對特定情況具有最后好的效果。為此,統計學在垃圾回收算法的發展中產生了很大的作用,現代的垃圾回收算法中按對象的存活時間,將內存的垃圾回收進行不同的分代,然后分別對不同的內存實施更高效的算法。

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

查看v8的源碼會發現關於新生代內存空間實際上為兩個獨立的新生代內存夠成,單個新生代內存設定在64位系統和32位系統上分別式16MB和8MB,所以新生代內存的最大值實際上是64位系統和32位系統分別是32MB和16MB。

同樣源碼中也可以看到老生代內存最大申請空間位64位系統和32位系統分別為1400MB和700MB,但v8的內存空間最大申請限制並不是直接將老新生代的最大限值加起來,而是(4*單個新生代內存限值+老生代內存限值),所以v8堆內存的最大限值在64位系統和32位系統上分別是(16*4+1400=1464MB)和(8*4+700=732MB)。

這也就是前面講到的64位系統上只能使用1.4GB內存和32位系統上只能使用0.7G內存的由來。

v8垃圾回收算法:Scavenge、Mark-Sweep、Mark-Compact、Incremental Marking

前面解析了內存限制和堆內存分代管理機制,接下來就通過垃圾回收算法來深度理解v8的垃圾回收機制。

Scavenge算法:

新生代的對象主要通過Scavenge算法進行垃圾回收處理,在Scavenge的具體實現中,主要采用了Cheney算法,該算法由C.J.Cheney於1970年首次發布在ACM論文上。

Cheney是一種采用復制的方式實現垃圾回收算法,它將內存一分為二,每一部分空間稱為semispace。在這兩個semispace空間中,只有一個處於使用中,另一個處於閑置狀態。處於使用狀態的semispace空間稱為From空間,處於閑置狀態的空間稱為To空間。

當分配對象時首先在From空間上進行分配,當開始進行垃圾回收時,會先檢查From空間中的存活對象,這些存活的對象將被復制到To空間中,而非存活對象占用的空間會被釋放。完成復制后,From空間和To空間的角色發生對換。簡單的說就是將存活對象在兩個semispace空間之間復制,這是典型的用空間換時間的算法,其缺點就是只能使用實際占用內存的一半空間,這也是前面提到的源碼中新生代內存空間實際上為兩個獨立的內存夠成的原因,這兩個獨立空間加上老生代對象使用的獨立內存空間,就是v8堆內存實際使用堆內存的內存大小。

Scavenge除了基於Cheney算法使用復制的方式釋放內存,還會根據檢查對象的內存地址是否經歷過一次Scavenge回收,如果經歷過一次會將對象從From空間復制到老生代空間中,如果沒有則復制到To空間中。

除了經歷過一次Scacenge回收的對象會被晉升以外,當To空間的內存占比超過25%時,當前檢查的對象就會直接晉升到老生代內存空間。下面是兩種新生代對象晉升的邏輯示意圖:

 

最后關於25%的限制值,是因為這個To空間在完成復制后要轉換成From空間,接下來還需要繼續為新的對象分配內存。

Mark-Sweep & Mark-Compact算法:

老生代空間中的對象因為存活占比較大,采用Scacenge的復制方式回收一旦存活對象較多,復制對象的效率就會降低,內存空間利用上也會浪費非常多。

由於老生代中的對象生命周期都會比較長,死亡對象占比較低,所以v8中使用Mark-Sweep標記清除和Mark-Compact標記整理的方式來實現內存回收。

Mark-Sweep標記清除:Mark-Sweep算法先遍歷堆中的所有對象,並標記出存活對象,然后清除沒標記的死亡對象。

 

Mark-Compact標記整理:Mark-Compact是由Mark-Sweep演變而來,它們的差比就是在對象標記死亡以后,在整理過程中將活着的對象往一端移動,然后清除死亡對象和已經被移動走的對象空間。

為什么有了Mark-Sweep標記清除還需要Mark-Compact標記整理呢?它們兩者又有什么區別呢?這是因為Mark-Sweep標記清除會導致內存碎片化,回對后續內存分配造成問題,比如在內存空間處於碎片化狀態時,突然需要分配一個大對象內存,而這些碎片化的內存空間里沒有這么大的內存片段就會出問題。為了解決這種碎片化內存的問題就有了Mark-Compact的標記整理,但是標記整理由於需要多做一個移動操作,所以它的速度肯定會比Mark-Sweep要慢。

但是需要注意Mark-Compact做移動操作是一個非常低效的操作,所以Mark-Sweep和Mark-Compact不僅僅是遞進關系,還是兩者結合使用的關系,Mark-Compact只有在空間不足以對新生代晉升過來的對象分配內存時才會使用。

Incremental Marking算法:

前面三種算法在進行垃圾回收的時候,都需要將應用邏輯暫停,等執行完垃圾回收后再恢復執行應用邏輯,這種行為被稱為“全停頓(stop-the-world)”。在新生代中做一次垃圾回收由於默認配置較小,需要消耗的時間不會太長,但老生代中1G多的內存遍歷標記整理操作絕對不會是一個短時間能完成。為了避免全堆垃圾回收帶來的應用邏輯長時間停頓問題,v8又引進了Incremental Marking增量標記,在前面三種回收算法基礎上來通過Incremental Marking增量標記實現“步進”式垃圾回收。

所謂增量標記實現“步進”式垃圾回收,就是將原本一次應用邏輯停頓完成全部標記操作拆分成許多個小“步進”,每做完一個“步進”就讓JavaScript應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行。所謂增量標記實現“步進”可以理解為一次完整的標記操作會被拆分成很多步來完成。

 

v8經過增量標記改進垃圾回收機制后,垃圾回收的最大停頓時間可以減少到原本的1/6左右。隨這v8的不斷發展,后續還引入了延遲清理(lazy sweeping)和增量式整理(incremental compaction),讓清理與整理動作也變成了增量式的操作。

2.2查看垃圾回收日志(監測v8垃圾回收性能)

2.2.1基於--trace_gc指令參數生成垃圾回收日志:

//index.js 測試生成垃圾回收日志的代碼
let a = [];
for(let i = 0; i < 1000000; i++){
    a.push(new Array(100));
}

測試指令:

node --trace_gc  .\index.js >gc.log 

執行完以后會在當前工作目錄下生成一個gc.log的垃圾回收日志文件,日志片段:

[1828:0000016323844600]       47 ms: Scavenge 2.3 (3.0) -> 1.9 (4.0) MB, 0.9 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
[1828:0000016323844600]       89 ms: Scavenge 2.9 (4.5) -> 2.7 (5.5) MB, 1.0 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
...

2.2.2基於--prof指令參數生成v8執行時的性能分析數據日志(還是使用前面的測試代碼):

node --prof .\index.js

執行完以后會在當前工作目錄下生成一個....v8.log日志文件,這個文件就是v8性能分析數據日志。

2.2.3基於nodejs內置統計日志工具統計日志信息:

Linux系統:linux-tick-processor

Windows系統:windows-tick-processor.bat

這個內置工具在Nodejs源碼的deps/v8/tools目錄下,將該目錄添加到環境變量中就可以直接通過啟動這個工具查看v8的運行日志。

 三、高效使用內存及內存指標

 3.1高效使用內存

在v8面前,開發者所要具備的責任是如何讓垃圾回收機制更高效。要討論這個話題就不得不了解JavaScript作用域、作用域鏈、閉包,這部分內容在之前的JavaScript的博客中有詳細的介紹,可以參考一下博客內容:javascript的作用域和閉包(三)閉包與模塊,這是一個系列博客的最后一篇,在這篇博客的開頭出列出了系列的其他博客連接,所以就不全部粘貼了。接下來就分析作用域、作用域鏈、閉包與內存之間的關系,以及如何在開發中實現高效的垃圾回收機制。

標識符查找:

JavaScript中的標識符可以理解為變量名,v8會從當前的作用域向全局作用域逐級查找,如果變量是全局變量,由於全局作用域要直到進程退出才能釋放,這種情況就會導致變量常駐在老生代內存空間中,通過前面對v8垃圾回收機制的分析知道老生代中的垃圾回收效率是比較低的,釋放全局作用域上的變量可以通過delete刪除引用關系。

如果變量是在非全局作用域上,想主動釋放變量引用的對象可以通過delete和重新賦值,並且兩者具有相同的效果。但v8中通過delete刪除對象的屬性有可能干擾v8的優化,所以通過賦值的方式解除引用更好。

通過重新賦值的方式解除引用實際上就是切斷變量對原對象內存的引用,並在內存中找一篇新的內存片段存放數據,原來的對象內存就會變成死對象,會被垃圾回收機制回收。

閉包與垃圾回收:

JavaScript中閉包可以實現在作用域外部引用作用域內部的變量,這得益於高階函數的特性。但同樣帶來比較嚴重的潛在風險,就是被外部引用的變量可能出現不會即時釋放的情況,同時由這個沒有及時釋放的變量導致作用域產生的內存占用也不會及時釋放。這種不及時釋放的閉包將會給程序帶來嚴重的內存管理風險,所以在JavaScript中有提出函數尾調用(JavaScript函數尾調用與尾遞歸)的編程范式來規避這種風險,雖然這不是一個完美的方案但還可以在一定程度上避免一些不必要的內存風險。

3.2內存指標:

前面提到過使用process.memoryUsage()可以查看內存使用情況,除此之外OS模塊中的totalmem()和freemenm()方法也可以查看內存使用情況。

查看進程的內存占用:

前面提到使用process.memoryUsage()可以查看Node進程的內存占用情況,為了更好的查看內存占用情況,這里先寫一個工具方法將字節為單位的數據轉換成MB單位:

 1 const showMem = function(){
 2     let mem = process.memoryUsage();
 3     let format = function(bytes){
 4         return (bytes / 1024 / 1024).toFixed(2) + 'MB';
 5     };
 6     console.log('rss:' + format(mem.rss) 
 7               + ' heapTotal: ' + format(mem.heapTotal) 
 8               + ' heapUsed: ' + format(mem.heapUsed) 
 9               + ' external: ' + format(mem.external) 
10               + ' arrayBuffers: ' + format(mem.arrayBuffers));
11     console.log('---------------------------------------------------------------------------------------------------');
12 };

然后在寫一個方法不停步的分配內存並不釋放內存,再查看打印的內存數據變化:

 1 let useMem = function(){
 2     let size = 20 * 1024 * 1024;
 3     let arr = new Array(size);
 4     for(let i = 0; i < size; i++){
 5         arr[i] = 0;
 6     }
 7     return arr;
 8 };
 9 
10 let total = [];
11 for(let j = 0; j < 15; j++){
12     showMem();
13     total.push(useMem());
14 }
15 showMem();

打印結果如下(部分打印結果,最后會出現內存溢出)

rss:18.02MB heapTotal: 3.83MB heapUsed: 2.16MB external: 0.76MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:180.61MB heapTotal: 164.51MB heapUsed: 162.66MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:341.23MB heapTotal: 325.51MB heapUsed: 322.45MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:501.57MB heapTotal: 487.77MB heapUsed: 482.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:661.83MB heapTotal: 651.77MB heapUsed: 642.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:822.14MB heapTotal: 819.77MB heapUsed: 802.48MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:982.73MB heapTotal: 995.78MB heapUsed: 962.45MB external: 0.94MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:1142.85MB heapTotal: 1156.03MB heapUsed: 1122.48MB external: 0.94MB arrayBuffers: 0.01MB
......

從測試的結果來看,只有前面的rss、heapTotal、heapUsed三個數據在不斷的增長,后面的external和arrayBuffer都幾乎不會發生變化。這里就先來分析前面三個數據:

rss是resident set size的縮寫,即進程的常駐內存部分,進程的內存總共分為三部分:一部分是srr常駐內存,其余是交換區swap或者文件系統中。

heapTotal和heapUsed對應的是v8的堆內存信息,heapTotal是總共申請的內存量,heapUsed表示目前堆中使用的內存量。

external是指綁定到v8管理的JavaScript對象的C/C++內存使用量,簡單的理解就是nodejs的C/C++擴展使用的內存數據。

arrayBuffers通常被稱為對外內存或者獨立內存,是由ArrayBuffer 和 SharedArrayBuffer 分配的內存。這里改造一下內存測試代碼:

 1 let useMem = function(){
 2     let size = 200 * 1024 * 1024;
 3     let buffer = new Buffer(size);
 4     for(let i = 0; i < size; i++){
 5         buffer[i] = 0;
 6     }
 7     return buffer;
 8 };
 9 let total = [];
10 for(let j = 0; j < 15; j++){
11     showMem();
12     total.push(useMem());
13 }
14 showMem();

查看打印測試結果:

rss:18.02MB heapTotal: 3.83MB heapUsed: 2.17MB external: 0.76MB arrayBuffers: 0.01MB
---------------------------------------------------------------------------------------------------
rss:221.02MB heapTotal: 4.50MB heapUsed: 2.85MB external: 200.94MB arrayBuffers: 200.01MB
---------------------------------------------------------------------------------------------------
rss:421.66MB heapTotal: 5.25MB heapUsed: 2.64MB external: 400.94MB arrayBuffers: 400.01MB
---------------------------------------------------------------------------------------------------
rss:622.00MB heapTotal: 7.75MB heapUsed: 2.02MB external: 600.93MB arrayBuffers: 600.01MB
---------------------------------------------------------------------------------------------------
rss:822.01MB heapTotal: 7.75MB heapUsed: 2.02MB external: 800.93MB arrayBuffers: 800.01MB
---------------------------------------------------------------------------------------------------
rss:1022.02MB heapTotal: 7.75MB heapUsed: 2.05MB external: 1000.93MB arrayBuffers: 1000.01MB
......

這時候你會發現external和arrayBuffer的數據飛速數增長,而heapTotal和heapUsed幾乎沒有變化。這一方面說明了nodejs可以突破v8的堆內存限制,另一方面說明由Buffer分配的內存其底層是由C/C++模塊實現,所以external的數據也跟隨arrayBuffers一起發生了變化。

nodejs通過Buffer突破v8的堆內存是因為nodejs作為JavaScript的服務端環境,需要考慮網絡流和文件I/O流對內存的需求。

查看系統的內存占用:

這里需要注意的是os需要使用require引入才能使用的模塊。

console.log(os.totalmem());//系統總內存大小
console.log(os.freemem()); //當前可用內存大小

 四、內存泄漏與內存泄漏排查

在v8的垃圾回收機制下,一般情況的代碼編寫很少會出現內存泄漏的情況,但內存泄漏往往都是在無意之間產生的,較難排查。盡管內存泄漏的情況不盡相同,但其實質只有一個,那就是應用當回收的對象出現意外沒有被回收,變成了常駐在老生代內存中的對象。

4.1通常造成內存泄漏的原因有這幾種:緩存、隊列消費不及時、作用域未釋放。

緩存:

因為內存比I/O的效率高,一旦命中內存中的緩存,就可以節省一次I/O的時間,這是一種極具誘惑的把內存當緩存使用的緣由。還有就是在JavaScript中直接使用對象的鍵值對來緩存數據,這種便捷性也是把內存當緩存使用的重要原因。緩存中存儲的鍵越多,長期存活的對象也就會越多,這將導致垃圾回收在進行垃圾回收和整理時,對這些對象做無用功。

雖然使用內存作為緩存有非常大的潛在風險,但有些功能中我們也不得不使用內存來作為緩存,只要我們在適當的時候釋放這些緩存或小心使用也不是不可以,下面就通過緩存限制策略來實現一個簡單的使用內存作為緩存的示例:

 1 //緩存限制策略
 2 const LimitableMap = function(limit){
 3     this.limit = limit || 10; //設置緩存對象的個數,默認為10個
 4     this.map = {};            //實現緩存的鍵值對
 5     this.keys = [];           //緩存鍵隊列
 6 };
 7 const hasOwnProperty = Object.prototype.hasOwnProperty;
 8 LimitableMap.prototype.set = function(key, value){
 9     let map = this.map;
10     let keys = this.keys;
11     if(!hasOwnProperty.call(map,key)){
12         if(keys.length === this.limit){
13             //如果當前緩存的key超出最大值,清除緩存中最早添加的key及key的內存對象的引用
14             let firstKey = keys.shift();
15             delete map[firstKey];
16         }
17         keys.push(key); //如果沒有key則添加
18     }
19     map[key] = value; //賦值,如果value被更新,會切斷之前的內存對象引用,重新申請一片新的內存作為新值的內存對象空間,之前的值引用的內存對象會被回抽
20 };
21 LimitableMap.prototype.get = function(key){
22     return this.map[key];   //使用緩存
23 };
24 module.exports = LimitableMap;

測試代碼:

1 const LimitableMap = require('./test.js');
2 let limitableBuffer = new LimitableMap(3);
3 limitableBuffer.set('a','aaa');
4 limitableBuffer.set('b','bbb');
5 limitableBuffer.set('c','ccc');
6 limitableBuffer.set('d','ddd');
7 console.log(limitableBuffer.get('a'), limitableBuffer.get('b'), limitableBuffer.get('c'), limitableBuffer.get('d'));
View Code

緩存解決方案:

緩存限制策略的淘汰策略並不高效,能應付一些小型的應用場景,如果需要更高效的緩存可以考慮Isaac Z. Schlueter采用LRU算法的緩存,guthub地址:https://github.com/isaacs/node-lru-cache

另外還可以考慮使用模塊機制通過exports導出函數,nodejs為了加速模塊的引入,所有模塊都會通過編譯執行,然后緩存起來,而模塊是常駐老生代的,但這種設計需要小心內存泄漏。

總的來說,直接使用進程內的內存作為緩存的方案都存在內存泄漏的風險,需要非常謹慎。如果有大量緩存的需求,還是建議使用進程的外部緩存軟件,外部緩存軟件有着良好的緩存過期淘汰策略及自右內存管理,不影響Node進程的性能。

使用外部緩存軟件的優勢:

-- 將緩存轉移到外部,減少常駐內存的對象數量,讓垃圾回收更高效。

-- 進程之間可以共享緩存。

目前市面上比較好的緩存軟件有:Redis和Memcached。

隊列狀態:

JavaScript中使用隊列來完成許多特殊需求,比如Bagpipe。隊列在消費者——生產者模型中充當中間產物,當消費速度小於生產速度就會形成堆積,造成內存泄漏。

比如在收集日志的時候,如果欠考慮也許會采用數據庫記錄日志,日志通常是海量的,而數據庫構建在文件系統上,寫入效率遠遠低於文件直接寫入,於事會形成數據庫寫入操作的堆積,而JavaScript中相關的作用域也不會得到釋放,從而導致內存泄漏。遇到這種情況選擇用文件寫入替換數據庫會更高效,但如果生產速度因為某些原因突然激增,或者消費速度因為故障降低,內存還是會可能出現泄漏的風險。

關於隊列堆積的情況解決方案,一方面可以通過監控隊列的長度,一旦堆積通過監控系統產生報警並通知相關人員解決;另一方面則是任意異步調用都應該包含超時機制,一旦在限定的時間內未完成響應,通過回調函數傳遞超時異常,使得任意異步調用的回調都具備可控的響應時間,給消費速度一個下限。

對於Bagpipe而言,它提供了超時模式和拒絕模式。超時響應超時錯誤,當隊列堆積時則對新到來的直接響應擁塞錯誤,這兩種模式都有效的防止了隊列擁塞的內存泄漏問題。

4.2內存泄漏排查

內存泄漏排查的常見工具:

v8-profiler:由Danny Coates提供,對v8堆內存抓取快照和對CPU進行分析,但很長時間沒有更新了。

node-heapdump:Nodejs核心貢獻者之一Ben Noordhuis編寫的模塊,它允許對v8堆內存抓取快照,用於時候分析。

node-mtrace:由Jimb Esser提供,它使用GCC的mtrace工具來分析內存泄漏。

dtrace:在Joyent的SmartOS系統上,有完善的dtrace工具用來分析內存泄漏。

node-memwatch:來自Mozilla的Lloyd Hilaiel貢獻的模塊,采用WTFPL許可發布。

4.2.1node-heapdump:

相關使用方法參考github官方文檔:https://github.com/bnoordhuis/node-heapdump

安裝注意事項:首先要確定當前設備安裝了python環境、visual C++ Build Tools,如果沒有安裝的花我個人建議自己到官網手動下載安裝即可,如果嫌麻煩可以通過下面這個命令以管理員的方式安裝:

npm install --g --production windows-build-tools

由於visual C++ Tools的相關安裝耗時較長,需要耐心的等,不要隨便中斷安裝操作,否則會導致重復安裝多個版本。然后還需要確定node全局下是否安裝了node-gyp。

node-gyp -v    //檢查是否安裝了node-gyp
npm install node-gyp -g    //如果沒有安裝node-gyp,就先安裝
npm heapdump    //最后在當前項目下安裝node-heapdump

確定上面的工具都安裝成功后,測試生成垃圾回收快照:

let heapdump = require('heapdump');
heapdump.writeSnapshot(function(err, filename) {
    console.log('dump written to', filename);
  });

測試生成快照這里我這里出現了一些暫時沒有解決的問題,官方文檔中給出了writeSnapshot兩種傳參方式,上面這一種我能測試成功在當前項目目錄下生了垃圾回收快照文件,但下面這種方式無法生成。

heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');

關於傳參問題暫時不研究,畢竟我是在windows系統下測試,真正的在服務端環境不一致,只要當前有一種方式可以實現就測試學習如何用node-heapdump排查內存泄漏。當然官方文檔中還給出了Linux的快照生成方式,啟動項目然后向進程發送 SIGUSR2 信號來強制創建快照:

kill -USR2 <pid>

最后就是將生成的.heapsnapshot快照文件用Chrome瀏覽器的dev tool,選中Memory然后點擊load按鈕打開剛剛生成的.heapsnapshot快照文件,查看內存的情況排查內存泄漏:

 

 然后就會進入到快照文件的詳細數據界面:

 

 關於具體如何分析這些數據就不在這里贅述了,如果有其他無法解決的問題就上網搜一下,資料還是挺多的,這里給一篇比較詳細的介紹操作說明鏈接:https://blog.csdn.net/cc18868876837/article/details/116714814

4.2.2node-memwatch:

相關使用方法參考github官方文檔:https://github.com/airbnb/node-memwatch

npm install @airbnb/node-memwatch

同樣需要注意設備上需要安裝python環境、visual C++ Build Tool相關環境和工具,由於我的當前設備環境支持其他工作,修改起來很麻煩,就不演示了。安裝相關環境和工具再粘貼一次:

npm install --g --production windows-build-tools //注意以管理員身份啟動命令工具安裝

官方文檔還是蠻詳細的,怎么用直接看官方文檔就OK了。這里有一篇《深入淺出nodejs》的node-memwatch筆記博客https://blog.csdn.net/ki4yous/article/details/107872537可以參考學習,我之前也是參考這本生學習的。

 五、大內存應用

nodejs作為js的服務端運行環境,操作大文件式必然的,由於v8的內存限制,不能直接使用fs.readFile()和fs.writeFile()進行大文件的操作,所以在nodejs擴展了基於流的I/O操作模塊stream,除了流的以外nodejs還有包含一系列操作I/O操作的其他模塊如Buffer、pipe。

 這里不對這些模塊和功能做具體分析,后面會有對應的博客更詳細的內容,這篇博客主要是解析nodejs的內存管理機制,然后在這部分與內存相關的大文件處理場景唯一要說明的就是,v8的內存限制不適合對大文件直接做讀寫操作,而應該使用基於v8內存限制之外的獨立內存(Buffer實現)和流的模式來處理大文件。

 1 //可以使用兩個文件測試一下這段代碼
 2 const fs = require('fs');
 3 
 4 let reader = fs.createReadStream('in.txt');
 5 let writer = fs.createWriteStream('out.txt');
 6 
 7 reader.on('data',function(chunk){
 8     writer.write(chunk);
 9 });
10 reader.on('end',function(){
11     writer.end();
12 });

由於讀寫模式固定,上面的代碼可以使用管道pipe簡化:

const fs = require('fs');

let reader = fs.createReadStream('in.txt');
let writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

關於nodejs的內存管理機制就分析到這里結束了,這篇博客主要介紹了:

--v8內存限制及其原因:單線程

--v8的垃圾回收機制及其相關算法:分代式管理

--如何基於v8實現高效的內存邏輯:避免把內存作為緩存、謹慎處理閉包相關邏輯、避免高頻的CPU消費業務邏輯,如果有相關業務盡量拆分成多個小的業務塊組合處理、及時釋放對象。

--內存泄漏的常見情況和解決方案:緩存(使用專業的緩存軟件)、隊列消費不及時(超時處理)、作用域未釋放(規范編碼模式)。

--內存泄漏排查:了解一些常見的內存泄漏排查工具。

--大文件應用:采用流的方式處理大文件。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM