这段代码已经在不少讲解内存泄漏的地方引用了,很是经典,因此拿出来做为第一个例子,如下是泄漏代码: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 💕~