理解闭包与内存泄漏

1、闭包的定义
闭包,是指有权访问另外一个函数做用域中变量的函数。从定义上咱们能够知道,闭包是函数,而且是被另外一个函数包裹的函数。因此须要用一个函数去包裹另外一个函数,即在函数内部定义函数。被包裹的函数则称为闭包函数,包裹的函数(外部的函数)则为闭包函数提供了一个闭包做用域,因此造成的闭包做用域的名称为外部函数的名称。npm

咱们先来看一个常见的闭包例子,如:浏览器

let foo;
function outer() { // outer函数内部为闭包函数提供一个闭包做用域(outer)闭包

let bar = "bar";
let inner = function() {
    console.log(bar);
    debugger; // 打一个debuuger断点,以便查看闭包做用域
    console.log("inner function run.");
}
return inner;

}
foo = outer(); // 执行外部函数返回内部函数
foo(); // 执行内部函数
咱们在浏览器上执行该段代码后,会停在断点位置,此时咱们能够看到造成的闭包做用域如图所示,
image函数

从图中咱们能够看到,造成的闭包做用域名称为外部的outer函数提供的做用域,闭包做用域内有一个变量bar能够被闭包函数访问到。工具

2、造成闭包的条件
从上面的闭包例子在,看起来造成的闭包的条件就是,一个函数被另外一个函数包裹,而且返回这个被包裹的函数供外部持有。其实,闭包函数是否被外部变量持有并不重要,造成闭包的必要条件就是,闭包函数(被包裹的函数)中必需要使用到外部函数中的变量。测试

function outer() { // outer函数内部为闭包函数提供一个闭包做用域(outer)ui

let bar = "bar";
let inner = function() {
    console.log(bar);
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函数中执行闭包函数inner

}
outer();
咱们稍微修改一下上面的例子,外部函数outer不将内部函数inner返回,而是直接在outer内执行。
imagethis

从执行结果能够看到,仍然造成了闭包,因此说这个被包裹的闭包函数是否被外部持有并非造成闭包的条件。debug

function outer() { // outer函数内部为闭包函数提供一个闭包做用域(outer)调试

let bar = "bar";
let inner = function() {
    // console.log(bar); // 注释该行,内部inner函数再也不使用外部outer函数中的变量
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函数中执行闭包函数inner

}
outer();
咱们再修改一下上面的例子,将console.log(bar)这行代码注释掉,这样inner函数中将再也不使用外部outer函数中的变量。
image

从执行结果上能够看到,没有造成闭包。因此造成闭包的必要条件就是,被包裹的闭包函数必须使用外部函数中的变量。

固然上面的结论也太过绝对了些,由于外部函数能够同时包裹多个闭包函数,也就是说,(外部)函数内部定义了多个函数,这种状况下,就不须要每一个闭包函数都使用到外部函数中的变量,由于闭包做用域是内部全部闭包函数共享的,只要有一个内部函数使用到了外部函数中的变量便可造成闭包。

function outer() { // outer函数内部为闭包函数提供一个闭包做用域(outer)

let bar = "bar";
let unused = function() {
    console.log(bar); // 再建立一个闭包函数,并在其中使用外部函数中的变量
}
let inner = function() {
    // console.log(bar); // 注释该行,内部inner函数再也不使用外部outer函数中的变量
    debugger;
    console.log("inner function run.");
}
inner(); // 直接在外部函数中执行闭包函数inner

}
outer();
咱们继续修改一下上面的例子,在outer函数内部再建立一个unused函数,这个函数只是定义但不会执行,同时unused函数内部使用了外部outer函数中的变量,inner函数仍然不使用外部outer函数中的变量。
image

从执行结果能够看到,又造成了闭包。因此造成的闭包条件就是,存在内部函数中使用外部函数中定义的变量。

3、内存泄漏
内存泄漏经常与闭包牢牢联系在一块儿,很容易让人误觉得闭包就会致使内存泄漏。其实闭包只是让内存常驻,而滥用闭包才会致使内存泄漏。
内存泄漏,从广义上说就是,内存在使用完毕以后,对于再也不要的内存没有及时释放或者没法释放。再也不须要的内存使用完毕以后确定须要释放掉,不然这个块内存就浪费掉了,至关于内存泄漏了。可是在实际中,每每不会经过判断该内存或变量是否再也不须要使用来判断。由于内存测试工具很难判断该内存是否再也不须要。因此咱们一般会重复屡次执行某段逻辑链路,而后每隔一段时间进行一次内存dump,而后判断内存是否存在不断增加的趋势,若是存在,则可用怀疑存在内存泄漏的可能。

4、内存dump
浏览器中抓取内存的dump相对来讲简单些,直接经过谷歌浏览器的调试工具找到memory对应的tab页面,而后点击Load便可开始抓取内存dump,如:
image

在NodeJS中,咱们也能够经过引入heapdump来抓取内存dump,直接经过npm安装heapdump模块便可

npm install heapdump
安装完成以后,便可直接在应用程序中使用了,用法很是简单,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump

// 应用code部分

heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
应用程序执行完成后,会在应用根目录中生成start.heapsnapshot和end.heapsnapshot两个内存dump文件,咱们能够经过判断两个文件的大小变化来判断是否存在内存泄漏。

固然并非说内存dump文件的大小不断增大就存在内存泄漏,若是应用的访问量确实在一直增大,那么内存曲线只增不减也属于正常状况,咱们只能根据具体状况判断是否存在内存泄漏的可能。

5、常见的内存泄漏
① 闭包循环引用

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
let foo = null;
function outer() {

let bar = foo;
function unused() { // 未使用到的函数
    console.log(`bar is ${bar}`);
}

foo = { // 给foo变量从新赋值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是这个对象携带的数据很是大,将会形成很是大的内存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
在这个例子中,执行了1000次outer函数,start.heapsnapshot文件的大小为2.4M,而end.heapsnapshot文件的大小为4.1M,因此可能存在内存泄漏。
前面讲解闭包的过程当中,咱们已经能够知道outer函数内部是存在闭包的,由于outer函数内部定义了unused和inner两个函数,虽然inner函数中没有使用到outer函数中的变量,可是unused函数内部使用到了outer函数中的bar变量,故造成闭包,inner函数也会共享outer函数提供的闭包做用域。
因为闭包的存在,bar变量不能释放,即至关于inner函数隐式持有了bar变量,因此存在...-->foo-->inner-->bar-->foo(赋值给bar的foo,即上一次的foo)...。
这里inner隐式持有bar变量怎么理解呢?由于inner是一个闭包函数,可使用outer提供的闭包做用域中的bar变量,因为闭包的关系,bar变量不能释放,因此bar变量一直在内存中,而bar变量又指向了上一次赋值给bar的foo对象,因此会存在这样一个引用关系。

那怎么解决呢?因为bar变量常驻内存不能释放,因此咱们能够在outer函数执行完毕的时候手动释放,即将bar变量置为null,这样以前赋值给bar的foo对象就没有被其余变量引用了,就会被回收了。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
let foo = null;
function outer() {

let bar = foo;
function unused() { // 未使用到的函数
    console.log(`bar is ${bar}`);
}

foo = { // 给foo变量从新赋值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是这个对象携带的数据很是大,将会形成很是大的内存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}
bar = null; // 手动释放bar变量,解除bar变量对上一次foo对象的引用

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
手动释放bar变量是一种相对比较好的解决方式。关键在于要解除闭包解除bar变量对上一次foo变量的引用。因此咱们可让unused方法内不使用bar变量,或者将bar变量的定义放在一个块级做用域中,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
let foo = null;
function outer() {

{ // 将bar变量定义在一个块级做用域内,这样outer函数中就没有定义变量了,天然inner也不会造成闭包
    let bar = foo;
    function unused() { // 未使用到的函数
        console.log(`bar is ${bar}`);
    }
}

foo = { // 给foo变量从新赋值
    bigData: new Array(100000).join("this_is_a_big_data"), // 若是这个对象携带的数据很是大,将会形成很是大的内存泄漏
    inner: function() {
        console.log(`inner method run`);
    }
}

}
for(let i = 0; i < 1000; i++) {

outer();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
② 重复注册事件,好比页面一进入就重复注册1000个同名事件(一次模拟每次进入页面都注册一次事件)

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
const events = require('events');
class Page extends events.EventEmitter {

onShow() {
    for (let i = 0; i < 1000; i++) {
        this.on("ok", () => {
            console.log("on ok signal.");
        });
    }
}
onDestory() {
    
}

}
let page = new Page();
page.setMaxListeners(0); // 设置能够注册多个同名事件
page.onShow();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
这个例子中Page页面一进入就会同时注册1000个同名的ok事件,start.heapsnapshot文件的大小为2.4M,而end.heapsnapshot文件的大小为2.5M,因此可能存在内存泄漏。
解决方式就是,在页面离开的时候移除全部事件,或者在页面建立的时候仅注册一次事件,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
const events = require('events');
class Page extends events.EventEmitter {

onCreate() {
    this.on("ok", () => { // 仅在页面建立的时候注册一次事件,避免重复注册事件
        console.log("on ok signal.");
    });
}
onShow() {
    // for (let i = 0; i < 1000; i++) {
    //     this.on("ok", () => {
    //         console.log("on ok signal.");
    //     });
    // }
}
onLeave() {
    this.removeAllListeners("ok"); // 或者在离开页面的时候移除全部ok事件
}

}
let page = new Page();
page.setMaxListeners(0); // 设置能够注册多个同名事件
page.onCreate();
page.onShow();
page.onLeave();
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
③ 意外的全局变量,这是咱们经常简单的内存泄漏例子,实际上内存工具很难判断意外的全局变量是否存在内存泄漏,除非应用程序不断的往这个全局变量中加入数据,不然对于一个恒定不变的意外全局变量内存测试工具是没法判断出是否存在内存泄漏的,因此咱们尽可能不要随意使用全局变量来保存数据。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump

function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(10000).join("this_is_a_big_data"));
}
return bigData;

}

function fn() {

foo = createBigData(); // 意外的全局变量致使内存泄漏

}
for (let j = 0; j < 100; j++) {

fn();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
该例子执行后,end.heapsnapshot文件的大小为2.5M也变成了2.5M,执行fn函数的时候意外产生了一个全局变量foo,并赋值为了一个很大的数据,若是foo变量用完后咱们再也不须要,那么咱们就要主动释放,不然常驻内存形成内存泄漏,若是这个全局变量咱们后续还须要使用到,那么就不算内存泄漏。
解决方法就是,将foo定义成局部变量,如:

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump

function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(10000).join("this_is_a_big_data"));
}
return bigData;

}

function fn() {

// foo = createBigData(); // 意外的全局变量致使内存泄漏
const foo = createBigData(); // 将foo定义为局部变量,避免内存泄漏

}
for (let j = 0; j < 100; j++) {

fn();

}
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
④ 事件未及时销毁

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
const events = require('events');
function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(100000).join("this_is_a_big_data"));
}
return bigData;

}

class Page extends events.EventEmitter {

onCreate() {
    const data = createBigData();
    this.handler = () => {
        this.update(data);
    }
    this.on("ok", this.handler);
}

update(data) {
    console.log("开始更新数据了"); // 接收到ok信号,能够开始更新数据了
}

onDestory() {
   
}

}
let page = new Page();
page.onCreate();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
此例中页面onCreate的时候会注册一个ok事件,事件处理函数为this.handler,this.handler的定义会造成一个闭包,致使data没法释放,从而内存溢出。
解决办法就是移除事件并清空this.handler,由于this.handler这个闭包函数被两个变量持有,一个是page对象的handler属性持有,另外一个是事件处理器因为注册事件后被事件处理器所持有。因此须要释放this.handler而且移除事件监听。

const heapdump = require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
const events = require('events');
function createBigData() {

const bigData = [];
for(let j = 0; j < 100; j++) {
    bigData.push(new Array(100000).join("this_is_a_big_data"));
}
return bigData;

}

class Page extends events.EventEmitter {

onCreate() {
    const data = createBigData();
    this.handler = () => {
        this.update(data);
    }
    this.on("ok", this.handler);
}

update(data) {
    console.log("开始更新数据了"); // 接收到ok信号,能够开始更新数据了
}

onDestory() {
    this.removeListener("ok", this.handler); // 移除ok事件,解决事件处理器对this.handler闭包函数的引用
    this.handler = null; //解除page对象对this.handler闭包函数的引用
}

}let page = new Page();page.onCreate();page.onDestory();heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump解除page对象和事件处理器对象对this.handler闭包函数的引用后,this.handler闭包函数就会被释放,从而解除闭包,data也会获得释放。

相关文章
相关标签/搜索