js 垃圾回收机制

内存的生命周期

内存分配 -> 使用内存 -> 释放内存

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 date = new Date(); // 分配一个 Date 对象

    var ele = document.createElement("div"); // 分配一个 DOM 对象
有些方法分配新的变量或者新对象:
    var s = "asdfsfd"
    var s2 = s.substr(0,3) // s2 是一个新的字符串
    // 因为字符串是不变量
    // JavaScript 可能决定不分配内存
    // 只是存储了[0-3] 的范围

    var a = ['abc', 'cbd'];
    var a2 = ['box', 'abs'];
    var a3 = a.concat(a2);
    // 新数组有四个元素, 是 a 连接 a2 的结果

js 内存的使用

使用值的过程实际上是对分配内存进行读取和写入操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
    var a = 10; // 分配内存
    console.log(a); // 对内存的读取使用

js 的内存回收

js 有自动垃圾回收机制, 那么这个垃圾自动回收机制的原理是什么呢? 其实很简单,就是找出那些不再继续使用的值,然后释放其内存。
多少内存管理的问题都在这个阶段。在这里最艰的任务是找到不再需要使用的变量。
不再需要使用的变量也就是在生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。
全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。
因为自动垃圾回收机制的存在,开发人员可以不关系也不注意内存释放的有关问题,但是无用内存释放这件事是客观存在的。不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况。

垃圾回收

引用

垃圾回收算法主要依赖于引用的概念
在内存管理感觉中,一个对象如果有访问另一个对象的权限(显示或者隐式),叫做一个对象引用另一个对象。
例如,一个javascript 对象具有对他原型的引用(隐式引用)和对它属性的引用(显示引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)

引用计数垃圾收集

这是最初的垃圾回收算法。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明对象已经不再需要了。
    var o = {
        a: {
            b: 2
        }
    };
    // 两个对象被创建, 一个作为另一个的属性被引用, 另一个被分配给变量。
    // 很显然,没有一个可以被垃圾收集

    var o2 = o; // o2 变量是第二个对“对象”的引用

    o = 1; // 现在, “这个对象” 的原始引用 o 被 o2 替换了

    var oa = o2.a; // 引用“这个对象”的 a 属性
    // 现在,“这个对象”有两个引用了, 一个是 o2, 一个是 oa

    o2 = "ya"; // 最初的对象现在已经是零引用了
                         // 它可以被垃圾回收了
                         // 然后它的属性 a 的对象还在被 oa 引用, 所以还不能回收
    oa = null; // a 属性的那个喜爱那个现在也是零引用了
                         // 它可以被垃圾回收了
由上面可以看出,引用计算算法是个简单有效的算法,但它却存在一个致命的问题: 循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
来看一个循环引用的例子:
    function f() {
        var o = {};
        var o2 = {};
        o.a = a2; // o 引用 o2
        o2.a = o; // o2 引用 o 这里

        return "azerty";
    }

    f();
上面我们申明了一个函数 f,其中包含两个相互引用的对象。在调用函数结束后,对象 o1 和 o2 实际已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。
再来看一个实际的例子:
    var div = document.createElement("div");
    div.onclick = function() {
            console.log("click");
    };
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。 此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。 一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。
为了解决循环引用造成的问题,现代浏览器通过使用标记清除算法来实现垃圾回收。

标记清除算法

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。 但反之未必成立。

工作流程

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

图片

循环引用不再是问题了

在看之前循环引用的例子:
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

函数调用返回之后,两个循环引用的对象在垃圾收集时从全局对象出发无法再获取他们的引用。 因此,他们将会被垃圾回收器回收。

内存泄露泄漏

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。
否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

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

内存泄漏的识别方法

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。 这就要求实时查看内存的占用情况。

在 Chrome 浏览器中,我们可以这样查看内存占用情况

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

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

  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字段为准。

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

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

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

在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数。 要记得回收这些回调函数。举一个 setInterval的例子:
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

闭包

在 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 可能会被调用,就会导致无法对其内存进行回收。
当这段代码被反复执行时,内存会持续增长。

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 元素的引用。

如何避免内存泄漏

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

参考

MDN-内存管理
JavaScript高级程序设计
JavaScript权威指南
JavaScript 内存泄漏教程
一种有趣的JavaScript内存泄漏
内存泄露

最近发现一个 好用的 node 调试方法

$ node - -inspect app.js  在平常启动项目的时候加上 --inspect 

 - -inspect参数是启动调试模式必需的。这时,打开浏览器访问

接下来,就要开始调试了。一共有两种打开调试工具的方法,第一种是在 Chrome 浏览器的地址栏,键入 chrome://inspect或者about:inspect,回车后就可以看到下面的界面。

在 Target 部分,点击 inspect 链接,就能进入调试工具了。

第二种进入调试工具的方法,是在 http://127.0.0.1:3000 的窗口打开”开发者工具”,顶部左上角有一个 Node 的绿色标志,点击就可以进入。

三、调试工具窗口
调试工具其实就是”开发者工具”的定制版,省去了那些对服务器脚本没用的部分。
它主要有四个面板。
Console:控制台
Memory:内存
Profiler:性能
Sources:源码
这些面板的用法,基本上跟浏览器环境差不多,这里只介绍 Sources (源码)面板。


四、设置断点
进入 Sources 面板,找到正在运行的脚本app.js。
在第11行(也就是下面这一行)的行号上点一下,就设置了一个断点。
这时,浏览器访问 http://127.0.0.1:3000/alice ,页面会显示正在等待服务器返回。切换到调试工具,可以看到 Node 主线程处于暂停(paused)阶段。
进入 Console 面板,输入 name,会返回 alice。这表明我们正处在断点处的上下文(context)。
再切回 Sources 面板,右侧可以看到 Watch、Call Stack、Scope、Breakpoints 等折叠项。打开 Scope 折叠项,可以看到 Local 作用域和 Global 作用域里面的所有变量。
Local 作用域里面,变量name的值是alice,双击进入编辑状态,把它改成bob。
然后,点击顶部工具栏的继续运行按钮。
页面上就可以看到 Hello bob 了。
命令行下,按下 ctrl + c,终止运行app.js。


五、调试非服务脚本
Web 服务脚本会一直在后台运行,但是大部分脚本只是处理某个任务,运行完就会终止。这时,你可能根本没有时间打开调试工具。等你打开了,脚本早就结束运行了。这时怎么调试呢?
$ node --inspect=9229 -e "setTimeout(function() { console.log('yes'); }, 30000)"
上面代码中,--inspect=9229指定调试端口为 9229,这是调试工具默认的通信端口。-e参数指定一个字符串,作为代码运行。
访问chrome://inspect,就可以进入调试工具,调试这段代码了。
代码放在setTimeout里面,总是不太方便。那些运行时间较短的脚本,可能根本来不及打开调试工具。这时就要使用下面的方法。
$ node - -inspect-brk=9229 app.js
上面代码中,--inspect-brk指定在第一行就设置断点。也就是说,一开始运行,就是暂停的状态。


六、忘了写 –inspect 怎么办?
打开调试工具的前提是,启动 Node 脚本时就加上--inspect参数。如果忘了这个参数,还能不能调试呢?
回答是可以的。首先,正常启动脚本。
$ node app.js
然后,在另一个命令行窗口,查找上面脚本的进程号。
$psax|grepapp.js
30464pts/11Sl+:00nodeapp.js
30541pts/12S+:00grepapp.js
上面命令中,app.js的进程号是30464。
接着,运行下面的命令。
$ node -e 'process._debugProcess(30464)'
上面命令会建立进程 30464 与调试工具的连接,然后就可以打开调试工具了。
还有一种方法,就是向脚本进程发送 SIGUSR1 信号,也可以建立调试连接。
$ kill -SIGUSR1 30464


七、参考链接
Debugging Node.js with Google Chrome, by Jacopo Daeli
Debugging Node.js with Chrome DevTools, by Paul Irish
Last minute node debugging, by Remy Sharp

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。我们经常会做些承诺,如果我赢了你就嫁给我,如果输了我就嫁给你之类的诺言。这就是promise的中文含义:诺言,一个成功,一个失败。)

语法:

new Promise( function(resolve, reject) {...} /* executor */  );

参数:

executor 

    executor 是一个带有 resolve 和 reject 两个参数的函数,executor 函数在 Promise 构造函数执行时同步执行,被传递 resolve  和 reject 函数 (executor 函数在 Promise 构造函数返回新建对象前被调用)。resolve 和 reject 函数被调用时,分别将 promise 的状态改为 fulfilled(完成)或 rejected(失败)。executor 内部通常会执行一个异步操作,一旦完成,可以调用 resolve 函数将 promise 状态改成 fulfilled,或者在发生错误是将它的状态改为 rejected

如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected 。executor 函数的返回值被忽略。


描述:

    Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。他允许你为异步操作的成功和失败费别绑定相应的处理方法(handlers)。这让异步方法可以像同步那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 Promise 对象

一个 Promise 有以下几种状态:

  • pending : 初始状态,成功或者失败状态
  • fulfilled:意味着操作成功完成。
  • rejected:意味着操作失败。

pending 状态的 Promise 对象可能触发 fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发失败状态(rejected)并传递失败信息。当其中任意一种情况出现是, Promise 对象的 then 方法绑定的处理方法(handlers)就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当 Promise 状态为 fulfilled 时, 调用 then 的 onfulfilled 方法, 当 Promise 状态为 rejected 时, 调用 then 的 onrejected 方法,所以在异步操作完成和绑定处理方法之间不存在竞争)。

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 对象, 所以它们可以被链试调用


不要被迷惑了: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如Scheme. Javascript中的promise代表一种已经发生的状态, 而且可以通过回调方法链在一起。 如果你想要的是表达式的延时计算,考虑无参数的"箭头方法":  f = () =>表达式 创建惰性求值的表达式使用 f() 求值。

注意: 如果一个promise对象处在fulfilled或rejected状态而不是pending状态,那么它也可以被称为settled状态。你可能也会听到一个术语resolved ,它表示promise对象处于fulfilled状态。(此处有待商榷。按本文的原英文翻译resolved表示fulfilled,但是后面的术语链接又表明resolved和fulfilled并不是一样。)关于promise的术语, Domenic Denicola 的 States and fates 有更多详情可供参考。

属性:

Promise.length 

    length 属性,其值总是为1(构造器参数的数目)

Promise.prototype

    表示 Promise 构造器的原型.


方法:

Promise.all(iterable) 

    这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。(可以参考jQuery.when方法---译者注)

Promise.race(iterable)

    当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

Promise.reject(reason)

    返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法

Promise.resolve(value)

    返回一个状态由给定value决定的Promise对象。如果该值是一个Promise对象,则直接返回该对象;如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。


创建 Promise

Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数会?把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数——resolve 和 reject ——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。

const myFirstPromise = new Promise((resolve, reject) => {
  // ?异步操作,最终调用:
  //
  //   resolve(someValue); // fulfilled
  // ?或
  //   reject("failure reason"); // rejected
});

想要某个函数?拥有promise功能,只需让其返回一个promise即可。

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
};



看了看官方文档, 感觉自己看不下去,我只能用零碎的记忆碎片,来说一下我对node 理解
首先我们先来感受一下node开发

const http = require("http");// 引入htpp 模块
http.creacteServer((req, res) => { // 创建一个服务
res.statusCode = 200;
res.Header("Content-Type", "text/plain");
res.end("Hello World");
})

node开发中,模块, 回调利用好, 那么你会在开发中得心应手, node 重要模块中为 express 这个模块非常重要