业务仔就写好业务,内存泄漏不是你该关心的问题

4,399 阅读8分钟

老是听到有人说内存泄漏,但是我做开发这么多年,除非我故意去测试内存是怎么泄漏的,这个时候才会内存泄漏,其他情况下几乎没有遇到过内存泄漏的问题。

然后就一直思考我没遇到内存泄漏的问题是不是跟我做的业务体量太小有关系?带着问题去找答案,今天就来聊聊内存泄漏。

什么是内存泄漏

内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

在我们最常听到的就是闭包没用好就容易造成内存泄漏,然而因为闭包产生的内存泄漏其实是很少的,因为内存泄漏的原因很多;

通常产生内存泄漏的原因有:

  • 闭包
  • 循环引用
  • DOM引用
  • 定时器
  • 事件监听
  • 作用域未释放
  • 递归调用
  • 全局变量
  • 未清理的大对象
  • 其他各种原因导致内存未释放的情况

在这里我就不一一介绍了,上面这么多种情况其实在JQuery支配的时代,造成内存泄漏的原因有两个,一个是DOM引用,一个是全局变量

因为JQuery的时代想要用单页面应用,切换页面的时候就会销毁一部分DOM,但是这个时候很多人会忘记把DOM的引用清除掉,导致DOM一直存在内存中,这就是DOM引用造成的内存泄漏;

还有一个就是全局变量,当时的单页面跨页面通信都是通过全局变量来实现的;

除了跨页面通讯还有开发人员的编码习惯,直接在js文件中的最顶层使用var来定义变量,或者是直接不使用var来定义变量,这样就会造成全局变量的污染,导致内存泄漏;

例如:

// 顶部定义变量
var a = 1;

// 顶部定义函数
function fn() {
  // 不适用 var 定义变量
  b = 2;
}

// dom 引用,在顶部定义,并且没有使用 var 定义
dom = document.getElementById('dom');

// 全局变量
var transmission = {
  a: 1,
  b: 2
}

上面这些情况大家应该都很常见,这些都是可能会造成内存泄漏的原因之一,但是用了这么多年也没见过内存泄漏的问题,这是为什么呢?

为什么会内存泄漏

上面说了内存泄漏的原因,但是要搞明白为什么会内存泄漏,就得先了解一下什么是内存

内存就是我们在定义变量的时候,给变量分配的空间,这个空间是有限的,当我们定义的变量超过这个空间的时候,就会造成内存溢出

内存泄漏就是我们定义的变量没有被释放,导致内存空间被占用,当我们定义的变量超过这个空间的时候,就会造成内存溢出

所以内存泄漏的本质指的是内存溢出,只不过内存溢出也有很多原因,内存泄漏只是其中的一种;

见证一下内存泄漏

上面说了这么多,我们来看一下内存泄漏的例子,也就是最开始我们提到的闭包

function cache() {
  var cache = {};
  return function(key, value) {
    if (cache[key] == null) {
      cache[key] = value;
    }
    
    return cache[key];
  }
}

var _cache = cache();
setInterval(function() {
  _cache(Math.random().toString(36).substr(2), 0);
}, 100);

上面这个例子是一个简单的缓存函数,我们可以通过这个函数来缓存一些数据,但是这个函数有一个问题,就是缓存的数据一直不会被释放,导致内存泄漏;

我们一秒钟就会缓存一个数据,这样就会导致内存一直增加,我们可以通过Performance来查看内存的使用情况,如下图:

image.png

可以看到内存曲线是一只在增加,没用下降的趋势,这种情况一般都是内存泄漏;

V8 的垃圾回收机制

上面我们已经知道了什么是内存泄漏,但是我的标题说内存泄漏并不是我们需要关心的,这里就要说到V8的垃圾回收机制了;

在网上查资料大多数人都能查到浏览器的垃圾回收机制分为两种,一种是标记清除,一种是引用计数

由于引用计数存在循环引用的bug,所以现在浏览器主流的垃圾回收机制基本上都是标记清除V8的垃圾回收机制是分代回收

分代回收指的是将内存分为新生代和老生代,新生代的内存空间比较小,老生代的内存空间比较大,新生代的内存空间会比较频繁的进行垃圾回收,当新生代的内存多次没有被回收的时候,就会被移动到老生代;

老生代的内存空间比较大,所以垃圾回收的频率比较低,但是垃圾回收的时候会暂停js的执行,所以垃圾回收的频率不能太高,否则会影响js的执行;

其实垃圾回收和内存泄漏的关系并不大,它关心的是有没有不需要用的内存需要回收,但是上面说到了垃圾回收的时候会暂停js的执行;

所以存在内存泄漏的时候,垃圾回收的执行时间会变长,这样就会影响js的执行,我们的页面就开始卡顿了;

不需要关心的内存泄漏

我们不需要关心的内存泄漏是因为浏览器运行在客户端,上面说到了在内存泄漏的时候,垃圾回收的执行时间会变长,这样就会影响js的执行,我们的页面就开始卡顿了;

作为一个普通用户,我发现页面突然变卡了,我这个时候直接刷新页面不就好了,这个时候内存会被重置,内存泄漏的问题就解决了;

所以老是纠结内存泄漏的问题,其实是不需要的,除了我上面说到的普通用户遇到卡顿的情况会直接刷新页面以外,浏览器在这一块也做了很多优化;

例如Chrome浏览器,当tab页没有被激活的时候,页面将会被冻结,这个时候页面的js执行就会被暂停,等重新激活的时候,页面的js执行就会继续;

这样就可以延长页面使用的时间,也就是延长了内存泄漏的时间,这样就可以避免内存泄漏的问题;

除了这个点以外,还有其他的优化,在通常情况除非你故意去制造内存泄漏,否则我们不需要关心内存泄漏的问题;

需要关心的内存泄漏

上面说到了不需要关心的内存泄漏,那么我们需要关心的内存泄漏是什么呢?

我们普通的业务仔几乎不需要关心,但是有些特殊的业务仔就需要关心了,例如页游直播canvaswebgl等等;

但是上面这些一样通过刷新页面就可以解决,需要关心,但是也可以接受,但是有一种情况就不一样了,那就是node

node运行在服务端,内存是非常宝贵的,如果内存泄漏,那么就会导致内存越来越少,最后就会导致node服务挂掉,这个可是大事;

node的内存泄漏和上面提到的内存泄漏是一样的,但是前端业务仔你需要写node的代码吗?所以前端业务仔不需要关心内存泄漏;

如何避免内存泄漏

上面说到了内存泄漏的问题,那么我们如何避免内存泄漏呢?

其实避免内存泄漏的方法很简单,就是设置一个阈值,当达到这个阈值的时候,就清空缓存;

拿我上面写的例子来说,我们可以设置一个阈值,当达到这个阈值的时候,就清除最早的缓存,这样就可以避免内存泄漏的问题;

function cache() {
  var cache = {};
  return function(key, value) {
    
    // 超过阈值,清除最早的缓存
    const keys = Object.keys(cache);
    if (keys.length > 10) {
      delete cache[keys[0]];
    }
    
    if (cache[key] == null) {
      cache[key] = value;
    }
    
    return cache[key];
  }
}

var _cache = cache();
setInterval(function() {
  _cache(Math.random().toString(36).substr(2), 0);
}, 100);

上面清除最早的值是错误的写法,因为使用对象作为缓存,对象的键值顺序不是固定的,只是为了演示才这样写。

上面设置阈值之后,内存不会一直增加了,最后会恒定在一个值,如下:

image.png

可以看到内存不会一直增加了,最后趋近于一个值,这样就避免了内存泄漏的问题;

总结

内存泄漏在前端其实是一个很常见的问题,明明很常见,面试经常问,实际很少去解决这个问题,因为我们不需要关心;

我们知道内存泄漏的原因,知道怎么解决就行了,但是并不需要过多的关心,是在不行让用户自己去刷新就行了;

如果哪天真的遇到这个问题需要解决,那么工作汇报不就有东西写了,升职加薪指日可待,哈哈哈