這段代碼已經在不少講解內存泄漏的地方引用了,很是經典,因此拿出來做爲第一個例子,如下是泄漏代碼:node
'use strict'; const express = require('express'); const app = express(); //如下是產生泄漏的代碼 let theThing = null; let replaceThing = function () { let leak = theThing; let unused = function () { if (leak) console.log("hi") }; // 不斷修改theThing的引用 theThing = { longStr: new Array(1000000), someMethod: function () { console.log('a'); } }; }; app.get('/leak', function closureLeak(req, res, next) { replaceThing(); res.send('Hello Node'); }); app.listen(8082);
js中的閉包很是有意思,經過打印heapsnapshot,在chrome的dev tools中展現,會發現閉包中真正存儲本做用域數據的是類型爲 closure
的一個函數(其__proto__指向的function)的 context
屬性指向的對象。c++
這個例子中泄漏引發的緣由就是v8對上述的 context
選擇性持有本做用域的數據的兩個特色:git
context
屬性指向的對象,而且其中只會包含全部的子做用域中使用到的父做用域變量。這種類型的泄漏本質上node中的events模塊裏的偵聽器泄漏,由於比較隱蔽,因此放在第二個例子,如下是泄漏代碼:github
const net = require('net'); let client = new net.Socket(); function connect() { client.connect(26665, '127.0.0.1', function callbackListener() { console.log('connected!'); }); } //第一次鏈接 connect(); client.on('error', function (error) { // console.error(error.message); }); client.on('close', function () { //console.error('closed!'); //泄漏代碼 client.destroy(); setTimeout(connect, 1); });
泄漏產生的緣由其實也很簡單:event.js
核心模塊實現的事件發佈/訂閱本質上是一個js對象結構(在v6版本中爲了性能採用了new EventHandles(),而且把EventHandles的原型置爲null來節省原型鏈查找的消耗),所以咱們每一次調用 event.on
或者 event.once
至關於在這個對象結構中對應的 type
跟着的數組增長一個回調處理函數。chrome
那麼這個例子裏面的泄漏屬於很是隱蔽的一種:net
模塊的重連每一次都會給 client
增長一個 connect事件
的偵聽器,若是一直重連不上,偵聽器會無限增長,從而致使泄漏。express
這個例子就比較簡單了,可是也屬於在失誤狀況下容易不當心寫出來的,如下是泄漏代碼npm
'use strict'; const easyMonitor = require('easy-monitor'); const express = require('express'); const app = express(); const _cached = []; app.get('/arr', function arrayLeak(req, res, next) { //泄漏代碼 _cached.push(new Array(1000000)); res.send('Hello World'); }); app.listen(8082);
若是咱們在項目中不恰當的使用了全局緩存:主要是指只有增長緩存的操做而沒有清除的操做,那麼就會引發泄漏。json
這種緩存引用不當的泄漏雖然簡單,可是我曾經親自排查過:Appium自動化測試工具中,某一個版本的日誌緩存策略有bug,致使搭建的server跑一段時間就重啓。數組
目前node上面用於排查內存泄漏的輔助工具也有一些,主要是:瀏覽器
這兩個工具的原理都是一致的:調用v8引擎暴露的接口: v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(title, control)
而後將獲取的c++對象數據轉換爲js對象。
這個對象中其實就是一個很大的json,經過chrome提供的dev tools,能夠將這個json解析成可視化的樹或者統計概覽圖,經過屢次打印內存結構,compare出只增不減的對象,來定位到泄漏點。
我以前項目中遇到疑似的內存泄漏基本都是這樣排查的,可是排查的過程當中也遇到了幾個比較困擾的問題:
因此後面花了點時間,詳細解析了下v8引擎輸出的heapsnapshot裏面的json結構,作了一個輕量級的線上內存泄漏排查工具,也是以前的Easy-monitor性能監控工具的一個補完。
對如何測試本身項目線上js代碼性能,以及找出js函數可優化點感興趣的朋友能夠參看這一篇:
本文下一節主要是以第I節中的三種很是典型的內存泄漏情況,來使用新一版的Easy-Monitor進行簡單的定位排查。
Easy-Monitor的使用很是簡單,安裝啓動總共三步
npm install easy-monitor
const easyMonitor = require('easy-monitor'); easyMonitor('你的項目名稱');
打開你的瀏覽器,輸入如下地址,便可看到進程相關信息:
http://127.0.0.1:12333
Easy-Monitor能夠實時展現內存分析信息,因此在線上使用也是沒有問題的,下面就來使用此工具分析第I節中出現的問題。
在閉包泄漏的代碼中,按照上面的步驟引入easy-monitor,而後不停在瀏覽器中訪問:
http://127.0.0.1:8082/leak
那麼幾回後經過top或者別的自帶內存監控工具能看到內存明顯上升:
這裏我本地訪問屢次後,已經飆升到211MB。
此時,咱們能夠在Easy-Monitor的首頁,點擊對應Pid後面的 MEM
連接,便可自動進行當前業務進程的堆內內存快照打印以及泄漏點分析:
大約等待10s左右,頁面即會呈現出解析的結果。最上面的 Heap Status
一欄呈現的內容是一個對當前堆內內存解析後的概覽,大概看看就好了,比較重要的泄漏點定位在下面的 Memory Leak
一欄。
我對疑似的內存泄漏點推測是從計算獲得的 retainedSize
着手的:泄漏的感知首先是內存無端增長,且只增不減,那麼當前堆內內存結構中從 (GC roots)
節點出發開始,佔據的 retainedSize
最大的就多是疑似泄漏點的起始。
遵循這個規則,Memory Leak
第一個子欄目獲得的是疑似泄漏點的概覽:
這裏按照 retainedSize
大小作了從大到小的排序,能夠看到,這幾個點基本上佔據了90%以上的堆內內存大小。
好了,下面的子欄目則是對這裏面的5個疑似泄漏點構建 引力圖,來找出泄漏鏈條,原理和前面同樣:佔據總堆內內存 retainedSize
最大的對象下面必定也有佔據其 retainedSize
最大的節點:
根據引力圖能夠很清晰看到 retainedSize
最大的疑似泄漏鏈條,顏色和大小的一部分含義:
這裏的展現用了Echarts2,全部的節點均可以點擊展開/摺疊。當咱們把鼠標移動到疑似泄漏鏈條的最後一個子節點時,引力圖下面會用文字顯示出當前的泄漏鏈條的詳細指向信息 Reference List
,這裏簡單的解析下其內容:
[object] (Route::@122187) ' stack ---> [object] (Array::@124261) ' [0] ---> [object] (Layer::@124265) ' handle ---> [closure] (closureLeak::@124169) ' context ---> [object] (system / Context::@84427) ' theThing ---> [object] (Object::@122271) ' someMethod ---> [closure] (someMethod::@122275) ' context ---> [object] (system / Context::@122269) ' leak ---> [object] (Object::@122113) ' someMethod ---> [closure] (someMethod::@122117) ' context ---> [object] (system / Context::@122111)
每一行表示一個節點:[類型] (名稱::節點惟一id) ’ 屬性名稱或者index。 由於測試代碼用了Express框架,熟悉Express框架源碼的小夥伴都能看出來了:
Route
的實例。Route
實例的 stack
屬性對應的數組的第一個元素,即這裏的 [0]
對應的元素,其實也就是一箇中間件,因此是 Layer
的一個實例。handle
屬性指向 closureLeak
函數,這裏開始出現咱們本身編寫的Express框架外的代碼了,簡單分析下也很容易明白這個中間件其實就是咱們編寫的app.get
部分。closureLeak
函數持有了上級做用域產生的閉包對象,這個閉包對象中 retainedSize
最大的變量爲 theThing
theThing
持有了 someMethod
的引用,someMethod
又經過上級做用域的閉包對象持有了 leak
變量,leak
變量又指向 theThing
變量指向的上一次的老對象,這個老對象中依舊包含了 someMethod
…經過這個引力圖和下面提供的 Reference List
分析,其實很容易發現泄漏點和泄漏緣由:正是由於第I節中提到的v8引擎做用域生成和持有閉包引用的規則,那麼 unused
函數的存在,致使了 leak
變量被 replaceThing
函數做用域生成的閉包對象存儲了,那麼 theThing
每一次指向的新對象裏面的 someMethod
函數持有了這個閉包對象,所以間接持有了上一次訪問 theThing
指向的老對象。因此每一次訪問後,老對象永遠由於被持有永遠沒法獲得釋放,從而引發了泄漏。
這裏也把關鍵詞整理出來,方便你們項目全局搜索排查:Leak Key
一樣的方式,第I節中的代碼保存後執行,注意 connect
操做的端口填寫一個本地不存在的端口,來模擬觸發客戶端的斷線重連。
那麼這段代碼跑大概一分鐘左右,即開始產生比較明顯的泄漏現象。一樣打開easy-monitor監控頁面進行堆內存分析,獲得以下結果:
這個圖很容易看出來,佔據 retainedSize
最大的對象正是 socket
對象,幾乎佔到了堆內總內存的 50% 以上。
接着往下看引力圖,以下所示:
其中的 Reference List
以下:
[object] (Socket::@97097) ' _events ---> [object] (EventHandlers::@97101) ' connect ---> [object] (Array::@102511)
這裏熟悉Node核心模塊 events
的小夥伴就能感到熟悉,_events
正是存儲訂閱事件/事件回調函數的屬性,那麼這邊很顯然是原生的socket觸發斷線重連時,會不停增長 connect
事件的處理,若是服務器一直掛掉,即客戶端沒法斷線重連成功,那麼內存就會不斷增長致使泄漏。
題外插一句,我翻了下net.js的代碼,這裏的 connect
事件是以 once
的方式添加的,因此只要重連過程當中可以連上一次,這部分偵聽器增長的內存就可以被回收掉。
這個是最簡單的緣由了,你們可使用Easy-Monitor自行嘗試一番~
根據第III節中的解析,明白了這種泄漏的原理,就比較容易對代碼進行修改了,斷掉 unused
函數對 leak
變量的引用,那麼 replaceThing
函數做用域的閉包對象中就不會有 leak
變量了,這樣 someMethod
即不會再對老對象間接產生引用致使泄漏,修改後代碼以下:
'use strict'; const express = require('express'); const app = express(); const easyMonitor = require('easy-monitor'); easyMonitor('Closure Leak'); let theThing = null; let replaceThing = function () { let leak = theThing; //斷掉leak的閉包引用便可解決這種泄漏 let unused = function (leak) { if (leak) console.log("hi") }; theThing = { longStr: new Array(1000000), someMethod: function () { console.log('a'); } }; }; app.get('/leak', function closureLeak(req, res, next) { replaceThing(); res.send('Hello Node'); }); app.listen(8082);
修改主要目的是在重連時去掉鏈接失敗時添加的 connect
事件,修改後代碼以下:
const net = require('net'); const easyMonitor = require('easy-monitor'); easyMonitor('Socket Leak'); let client = new net.Socket(); function callbackListener() { console.log('connected!'); }); function connect() { client.connect(26665, '127.0.0.1', callbackListener} connect(); client.on('error', function (error) { // console.error(error.message); }); client.on('close', function () { //console.error('closed!'); //斷線時去掉本次偵聽的connect事件的偵聽器 client.removeListener('connect', callbackListener); client.destroy(); setTimeout(connect, 1); });
修改和測試你們能夠自行嘗試一番。
作這個工具也讓本身對於v8的內存管理有了更深刻的認識,收穫挺大的,下一步的計劃是優化代碼邏輯和前臺呈現界面,提升易用性和開發者的體驗。
Easy-Monitor新版本下依舊支持線上部署和多項目cluster部署,最後項目的git地址在:
若是你們以爲有幫助或者不錯,歡迎給個star 💕~