Bootstrap

什么是内存泄漏?Chrome浏览器中怎么查看内存占用情况?

概述:

内存泄漏:JS里已经分配内存地址的对象,但是由于长时间没有释放或没办法清除,造成长期占用内存的现象,最终导致运行速度慢,甚至崩溃的现象。

JavaScript,会在创建变量时自动分配内存,并且在不再使用它们时“自动”释放内存,这个自动释放内存的过程称为垃圾回收

因为自动垃圾回收机制的存在,让大多Javascript开发者感觉他们可以不关心内存管理,所以会在一些情况下导致内存泄漏。

内存的生命周期

JS 环境中分配的内存一般有如下生命周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

JS 的内存分配

为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
var o = {
 a: 1,
 b: null
}; // 给对象及其包含的值分配内存
// 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"]; 
function f(a){
 return a + 2;
} // 给函数(可调用的对象)分配内存
// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
 someElement.style.backgroundColor = 'blue';
}, false);

有些函数调用结果是分配对象内存:

var d = new Date(); // 分配一个 Date 对象
var e = document.createElement('div'); // 分配一个 DOM 元素

有些方法分配新变量或者新对象:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新数组有四个元素,是 a 连接 a2 的结果

JS 的内存使用

使用值的过程实际上是对分配内存进行读取与写入的操作。

读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

var a = 10; // 分配内存
console.log(a); // 对内存的使用

JS 的内存回收

JS 有自动垃圾回收机制,那么这个自动垃圾回收机制的原理是什么呢?

其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。

大多数内存管理的问题都在这个阶段。

在这里最艰难的任务是找到不再需要使用的变量。

  • 不再需要使用的变量也就是生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用,那么该变量会被标记回收。(闭包除外)
  • 全局变量的生命周期直至浏览器关闭页面才会结束,也就是说全局变量不会被当成垃圾回收。

因为自动垃圾回收机制的存在,开发人员可以不关心也不注意内存释放的有关问题,但对无用内存的释放这件事是客观存在的。

不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况。

垃圾回收算法:

    栈堆空间分配区别:
      1.栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
      1.堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。

    js垃圾回收机制-算法说明(常见两种):引用计数法和标记清除法
      1、引用计数法
        IE采用的引用计数法,定义”内存不再使用“,就是看一个 对象 是否 指向 它的引用,没有引用了就回收对象
        算法:
          1.跟踪记录被 引用的次数
          2.如果被引用一次,那么就记录次数1,多次引用会 累加++
          3.如果减少一个引用就 减1 --
          4.如果引用次数是0,则释放内存
        但它却存在一个致命的问题:嵌套引用(循环引用)
        如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄漏。
            function fn2() {
              let o1 = {};
              let o2 = {};
              o1.a = o2;
              o2.a = o1;
              return '引用计数无法回收'; // 因为他们的引用次数永远不会是0
            }
            fn2() 
      2、标记清除法
          现代的浏览器已经不再使用引用计数法了。
          现代浏览器通用的大多是基于 标记清除法 的某些改进算法,总体思想都是一致的。
          核心:
            1.标记清除法将”不再使用的对象“定义为”无法达到的对象“
            2.就是从 根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从 根部到达 的对象,都是还 需要使用 的。
            3.那些 无法 由根部触发触及到的 对象被标记 为不再使用,稍后进行回收。

            从根部进行扫描,能查到的继续使用,查不到的进行标记稍后回收
     */

一、什么是内存泄漏

程序的运行需要内存。只要程序提出要求,操作系统或运行时(runtime)就必须提供内存。对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

本质上讲,内存泄漏就是由于疏忽或错误造成程序未能释放那些不再使用的内存,照成内存的浪费。

简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

参考链接:https://www.toutiao.com/a6709023162064962051/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1567217731&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&req_id=20190831101530010152039098792EF53&group_id=6709023162064962051

造成内存泄漏的因素: 

  1. 一些未声明直接赋值的变量;
  2. 一些未清空的定时器;
  3. 过渡的闭包;
  4. 一些引用元素没有被清空

常见的内存泄露案例:

1.意外的全局变量

function foo() {
 bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
 this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();

在这个例子中,意外的创建了两个全局变量 bar1 和 bar2

2.被遗忘的定时器和回调函数

在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数。

要记得回收这些回调函数。举一个 setInterval的例子:

var serverData = loadData();
setInterval(function() {
 var renderer = document.getElementById('renderer');
 if(renderer) {
 renderer.innerHTML = JSON.stringify(serverData);
 }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。

但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收,

定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

3.闭包

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包。 

/* 
    1.如何产生闭包?
        当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包
    2.闭包到底是什么?
        闭包 = 内层函数 + 外层函数的变量
        理解一:闭包是嵌套的内部函数(绝大部分人)
        理解二:包含被引用变量(函数)的对象(极少数人)
        注意:闭包存在于嵌套的内部函数中
    3.产生闭包的条件?
        函数嵌套
        内部函数引用了外部函数的数据(变量/函数)
    4.闭包的作用?
        封闭数据,实现数据私有,外部也可以访问函数内部的变量
        闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
    5.使用场景
        1.创建私有变量
        2.延长变量的生命周期
    6.闭包可能引起的问题?
        内存泄漏
*/

function outer() {
    var a = 2;
    function fn() { // 执行函数定义就会产生闭包(不用调用内部函数)
        console.log(a);
    }
}
outer();
闭包的常规用法
// 闭包的常规用法
/** 
 * 1.将函数作为另一个函数的返回值
 * 2.将函数作为实参传递给另一个函数调用
*/

function fn1() {
    var a = 2;
    function fn2() {
        a++;
        console.log(a);
    }
    return fn2;
}
var fn = fn1();
fn();
fn();

// 2.将函数作为实参传递给另一个函数调用
function showDelay(msg,time) {
    setTimeout(() => {
        alert(msg);
    }, time);
}
showDelay("4444",1000);

/** 
 * 闭包的作用:
 *  1.使用函数内部的变量在函数执行后,仍然存活在内存中(延长了局部变量的生命周期)
 *  2.让函数外部可以操作(读写)到函数内部的数据(变量/函数)
 * 
 * 问题:
 *  1.函数执行完后,函数内部声明的局部变量是否还存在?  一般是不存在,存在于闭包中的变量才可能存在
 *  2.在函数外部能直接访问函数内部的局部变量吗?  不能,但我们可以通过闭包让外部操作它
 * 
*/

/** 
 * 闭包的生命周期:
 *  1.产生:在嵌套内部函数定义执行完成时就产生了(不是在调用)
 *  2.死亡:在嵌套的内部函数成为垃圾对象时
*/
// fn = null; // 闭包死亡(包含闭包的函数对象成为垃圾对象---回收闭包)


/** 
 * 1.闭包的缺点:
 *  函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
 *  容易造成内存泄漏
 * 2.解决:
 *  能不用闭包就不用
 *  及时释放
*/

在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。

下面这种情况下,闭包也会造成内存泄露:

var theThing = null;
var replaceThing = function () {
 var originalThing = theThing;
 var unused = function () {
 if (originalThing) // 对于 'originalThing'的引用
 console.log("hi");
 };
 theThing = {
 longStr: new Array(1000000).join('*'),
 someMethod: function () {
 console.log("message");
 }
 };
};
setInterval(replaceThing, 1000);

这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。

同时 unused 是一个引用了 originalThing 的闭包。

这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。

当这段代码被反复执行时,内存会持续增长。

4.DOM 引用

很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。

var elements = {
 image: document.getElementById('image')
};
function doStuff() {
 elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
 document.body.removeChild(document.getElementById('image'));
 // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}

上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。

另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。

举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。

但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。

这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。

二、内存泄漏的识别办法

经验法则是,如果连续5次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。

这就要求实时查看内存的占用情况。

三、在Chrome浏览器中,我们怎么查看内存占用情况?

  1. 打开开发者工具,选择 Performance 面板
  2. 在顶部勾选 Memory
  3. 点击上角的 record 按钮
  4. 在页面上进行各种操作,模拟用户的使用情况
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况(如下图)

1.

2.

3.

4.

我们有两种方式来判定当前是否有内存泄漏:

  1. 多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏
  2. 某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏

在服务器环境中使用 Node 提供的 process.memoryUsage 方法查看内存情况

console.log(process.memoryUsage());
// { 
// rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 
// }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。

该对象包含四个字段,单位是字节,含义如下:

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

四、如何避免内存泄漏

记住一个原则:不用的东西,及时归还。

  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

;