《大前端进阶Node.js》系列 内存泄漏入门

2,234 阅读9分钟

前言

Coding 应当是一生的事业,而不仅仅是 30 岁的青春饭 本文已收录 Github https://github.com/ponkans/F2E,欢迎 Star,持续更新

大爷: 看小伙子眉清目秀,不妨先跟我讲讲什么是内存泄漏?

小伙:(心里一阵暗喜)官方解释是程序中己动态分配的堆内存由于某种原因程序未释放或无法释放.

但其本质其实就是一个,那就是应当回收的对象没有被回收,变成了常驻在老生代中的对象。

很多人说闭包会造成内存泄漏,其实说法不严谨,应该说是,闭包如果使用不当,容易引发内存泄漏,而不是闭包一定会造成内存泄漏。

大爷:小伙子别激动,那你知道 Node.js 中造成内存泄漏的原因一般有哪些嘛?

小伙:嗯,这个我也知道。可以从缓存的角度来分析。

缓存

缓存在项目中是很有作用的,因为一旦命中缓存,就可以节约一次I/O的时间,并且访问效率比I/O要高很多。

比如下面这种方式:

let cache = {}
const setValue = function (value) {
    cache[key] = value
}
const getValue = function () {
    if (cache[key]) return cache[key]
    // 从其它渠道获取
}

我们都知道 V8 的垃圾回收机制是分新生代跟老生代的(如果不熟悉的小伙伴,可以留言,后面出一篇 V8 垃圾回收机制)。

那么一旦一个对象被当做缓存来使用,那就意味着这个对象会常驻老生代。

同时随着你缓存对象的不断增多,缓存对象也会越来越多,越来越大,垃圾回收在进行处理的时候,就会做很多无用功。

因此,上面的使用方式实际上存在一些问题:

  • 未限定缓存对象大小
  • 没有实现过期策略,也就是缓存淘汰策略,不然内存会无限制增长,数据一旦多了,很容易出现内存泄漏,一些非热点的数据过多的堆积在内存,导致内存瞬间冲到峰值

因此,需要对上面的用法,接着改进,接着奏乐~

改进方式:FIFO-Cache

class Cache {
    constructor (limit = 5) {
        this.limit = limit
        this.map = {}
        this.keys = []
    }
    
    set (key,value) {
        let map = this.map
        let keys = this.keys
        if (!Object.prototype.hasOwnProperty.call(map,key)) {
            if (keys.length === this.limit) {
              // 先进先出的策略进行淘汰
                delete map[keys.shift()]
            }
            keys.push(key)
        }
        map[key] = value
    }
    
    get (key) {
        return this.map[key]
    }
}

实现方式很简单,一旦key超过设定的限制值,就以队列先进先出的方式进行淘汰。

大爷:小伙子,你上面这种方式只能针对一些比较简单的场景,如果需要更加高效的缓存,是否可以再优化?

小伙:这。。小伙欲言又止,始终还是没有说出那三个字~

LRU:最近被访问的数据那么它将来访问的概率就大,缓存满的时候,优先淘汰最无人问津者。相比FIFO,会更加精准跟高效

继续优化,LRU-Cache

    get (key) {
        if (this.cache.has(key)) {
            let cache = this.cache
            const value = cache.get(key)
            cache.delete(key)
            cache.set(key, value)
            return value
        }
        return '其它渠道获取'
    }

    set (key, value) {
        let cache = this.cache
        if (cache.has(key)) {
            cache.delete(key)
        } else if (cache.size === this.limit) {
            cache.delete(cache.keys().next().value)
        }
        cache.set(key, value)
    }
} 

以个人的经验来看,在Node中,任何试图拿内存来做缓存的行为都应该被限制(被限制不是说完全不能使用,但是需要谨慎使用)。

在特定场景下,还是可以使用的,毕竟走本地内存一定会比走网络(比如Redis服务)更快

  • 作为核心数据兜底,比如希望从 Redis 未取到数据时,在走一个本地的内存缓存兜底
  • 针对一些特殊的场景,可以在结合 LRU 算法做缓存,这样也不会造成前面说的内存被无限制使用

因此,直接将内存作为缓存存在很多的问题,需要开发者考虑的事情很多。

除了前面说的限制缓存大小,设置缓存淘汰等机制以外,还需要考虑进程间重复的缓存如何共享(毕竟进程之间无法共享内存)。

所以,我们可以将缓存转移到外部,不仅可以减少内存中缓存对象数量,让垃圾回收更加高效,同时还实现了进程间共享缓存。

进程之间共享缓存是很多场景下都是很有必要的,可参考《大前端进阶 Node.js》系列 双十一秒杀系统,里面有提到这个问题

个人比较推荐Redis,各方面都做的很成熟,使用 Redis,上面提到的问题都不是问题了~

总结

本文已收录 Github https://github.com/ponkans/F2E,欢迎 Star,持续更新

  • 内存做缓存需考虑的方面较多,需谨慎使用,不然容易引发内存泄漏
  • 如果需要使用大量缓存,建议使用进程之外的缓存,个人推荐 Redis