【原创】NodeJs内存泄漏示例学习

内存泄漏就是应该被回收的内存,因为被标记为可达到对象而没有被正常回收。可达到的对象可以简单理解为被引用,但在实际情况中可能因为闭包等隐式引用而不易被发觉,下面几种常见的场景列举了一些内存泄漏的示例,供学习分析。

相关文章

目录

  • 全局变量
  • 闭包
  • 缓存
  • 监听器

全局变量

全局变量不会被GC,因此使用全局变量(尤其是全局变量为对象且会增长的时候)要在不需要的时候将变量删除,或将变量设置为nullundefined

// 定义全局变量
var1 = 'var1' // 不推荐写法,且在严格模式下会报错
global.var2 = 'var2'
global.var3 = []

// 释放全局变量
global.var1 = null
global.var2 = undefined
delete global.var3

闭包

闭包:实现外部作用域访问内部作用域中变量的方法。这得益于高阶函数的特性:函数可以作为参数或返回值。

闭包是一个造成内存泄漏比较常见的场景。一个简单的闭包形式如下:

const func = function () {
  const data = 'inner variable'
  return () => {
    return data
  }
}

// 实现了外部访问 func 内部作用域的变量data
const getData = func()
console.log(getData())

下面介绍一个在闭包中产生内存泄漏的经典示例(引用于:https://github.com/ElemeFE/node-interview/issues/7

function format (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}

let theThing = null
let replaceThing = function () {
  let leak = theThing
  let unused = function () {
    if (leak) { console.log('hi') }
  }
  // 不断修改引用
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log('a')
    }
  }

  global.gc() // 手动触发GC,保证能回收的内存都回收了
  console.log(`heapUsed: ${format(process.memoryUsage().heapUsed)}`)
}
setInterval(replaceThing, 100)

执行node -expose-gc test7.js,输出:

heapUsed: 4.31 MB
heapUsed: 5.27 MB
heapUsed: 6.23 MB
heapUsed: 7.18 MB
heapUsed: 8.13 MB

从运行结果中看到内存不断上升。

该代码是想通过replaceThing方法每次重新生成一个新对象,但之前生成的对象没有释放导致内存泄漏。从代码上分析,leak变量虽然引用了theThing,且在unused方法中引用了,但unused方法未被引用,因此不因发生内存泄漏。这里涉及到了另一个知识,如下:

闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量。

结合以上示例分析,即在replaceThing的作用域中存储的变量是它内部函数unusedsomeMethod所有使用到的变量,所以leak变量虽然未被someMethod方法引用,但someMethod引用的闭包的变量包含leak。所以整体的引用关系可以梳理如下:

  1. theThing引用了longStr(无法释放)、someMethod
  2. someMethod引用了leak
  3. leak引用了上一次执行的theThinglet leak = theThing
    ......1、2、3循环引用

所有创建的theThing对象都无法释放,导致内存泄漏。要解除这里的循环引用,需要只要打破这种循环引用的关系,以下介绍两种方式,应根据实际代码逻辑选择。

// 解决方法一:删除leak变量,直接引用最外层的theThing
let theThing = null
let replaceThing = function () {
  let unused = function () { // eslint-disable-line
    if (theThing) { console.log('hi') }
  }
  // 不断修改引用
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log('a')
    }
  }

  global.gc() // 手动触发GC
  console.log(`heapUsed: ${format(process.memoryUsage().heapUsed)}`)
}
setInterval(replaceThing, 100)
// 解决方法二:将unused方法引用的变量改为参数传递
let theThing = null
let replaceThing = function () {
  let leak = theThing
  let unused = function (thing) { // eslint-disable-line
    if (thing) { console.log('hi') }
  }
  // 不断修改引用
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log('a')
    }
  }

  global.gc() // 手动触发GC
  console.log(`heapUsed: ${format(process.memoryUsage().heapUsed)}`)
}
setInterval(replaceThing, 100)

缓存

javascript中,使用对象来缓存一些计算结果是很容易的,可以避免重复计算一些相同结果。lodash库中memoize方法实现对函数的计算结果缓存,默认以第一个参数作为缓存的key。源码如下:

function memoize(func, resolver) {
  if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
    throw new TypeError('Expected a function')
  }
  const memoized = function(...args) {
    // 以第一个参数作为key,可传递resolver方法自定义key
    const key = resolver ? resolver.apply(this, args) : args[0]
    const cache = memoized.cache

    // 缓存存在,直接返回
    if (cache.has(key)) {
      return cache.get(key)
    }
    // 调用执行方法,并缓存到cache中
    const result = func.apply(this, args)
    memoized.cache = cache.set(key, result) || cache
    return result
  }
  memoized.cache = new (memoize.Cache || MapCache)
  return memoized
}

在浏览器端,由于方法调用次数有限,缓存的key不多,且浏览器页面刷新频率较高,缓存占用的内存不会很高。然后在node服务器中,参数种类多、服务器运行时间长会导致缓存的key较多,且占用的内存一直不能得到释放,因此需要谨慎地使用此类缓存。可以控制key可缓存的最大数,也可以在合适的时机清理掉一些过时的缓存。

当然,考虑分布式服务部署、多进程和缓存的高效管理,缓存最好借用外部工具来保证服务不保存任何状态,如redis

监听器

先看下面连接socket的示例

const net = require('net')
let client = new net.Socket()

function format (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}

function callbackListener () {
  console.log('connected!')
};

function connect () {
  client.connect(8000, '127.0.0.1', callbackListener)
}

connect()

client.on('error', (error) => { // eslint-disable-line
  console.log(`heapUsed: ${format(process.memoryUsage().heapUsed)}`)
})

client.on('close', function () {
  client.destroy()
  setTimeout(connect, 1)
})

输出:

heapUsed: 3.83 MB
heapUsed: 3.89 MB
heapUsed: 3.91 MB
heapUsed: 3.91 MB
heapUsed: 3.92 MB
heapUsed: 3.92 MB
heapUsed: 3.93 MB
heapUsed: 3.94 MB
heapUsed: 3.94 MB
heapUsed: 3.95 MB
(node:12708) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit

堆内存的使用量一直在上升,在第11次的时候提示监听器的数量操作了最大值10。

在NodeJs中,事件监听使用EventEmitter类,EventEmitter类包含一个listeners数组,存放所有的监听回调的方法,listeners组件默认最大值为10。上面的示例里,每次调用connect方法都会添加一个监听器,因此监听器一直增长,导致内存泄漏。

解决方式如下:

client.on('close', function () {
  // 移除之前的监听器
  client.removeListener('connect', callbackListener);
  client.destroy()
  setTimeout(connect, 1)
})

通常EventEmitter的实例对象不会释放,所以重复添加的监听器也不能得到释放。以这个示例为例,其他的监听器同样需要考虑是否有重复添加监听器的情况。

本文参考资源如下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354