Node_深刻淺出Node

NodeJs簡介

Ryan Dahl項目命名爲:web.js 就是一個Web服務器.
單純開發一個Web服務器的想法,變成構建網絡應用的一個基本框架.
Node發展爲一個強制不共享任何資源的單線程,單進程系統。
每個Node進程都構成這個網絡應用中的一個節點,這是它名字所含意義的真諦。node

Node的誕生歷程

  • 2009年3月,Ryan Dahl在博客宣佈並建立ios

  • 2009年5月,在GitHub上發佈最初的版本web

  • 2009年12月和2010年4月,兩屆JSConf大會安排了Node的講座算法

  • 2010年末,Ryan Dahl加入Joyent全職負責Node的發展sql

  • 2011年7月,發佈Windows版本chrome

  • 2011年11月,成爲GitHub上面關注度最高的項目數據庫

  • 2012年1月底,Ryan Dahl 將掌門人身份交給NPM的做者Issac Z.Schlueterexpress

  • 2013年7月,發佈穩定版V0.10.13npm

  • 隨後,Node的發佈計劃主要集中在性能上面,V0.14後正式發佈V1.0版本編程

選擇JavaScript

  • 高性能(chrome的V8引擎的高性能)

  • 符合事件驅動(JavaScript在瀏覽器中有普遍的事件驅動方面的應用)

  • 沒有歷史包袱(爲其導入非阻塞的I/O庫沒有而外阻力)

Node給JS帶來的意義

  • Node結構與Chrome十分類似,基於事件驅動的異步架構

  • Node中JS能夠訪問本地文件,搭建服務器,鏈接數據庫

  • Node打破了過去JS只能在瀏覽器中運行的局面,先後端編程環境統一

clipboard.pngclipboard.png

Node特色

異步I/O
事件與回調函數
單線程

* child_progress:解決單線程中大量算量的問題
* Master-Worker:管理各個工做進程

跨平臺:兼容Windows和*nix平臺

構建異步I/O,從文件讀取到網絡請求。
能夠從語言層面很天然的進行並行I/O 操做。每一個調用之間無序等待以前I/O調用結束。

clipboard.png

事件編程方式:輕量級,鬆耦合,只會關注事務點。

單線程弱點:

  • 沒法利用多核CPU

  • 錯誤會引發整個應用退出,應用的健壯性

  • 大量計算佔用CPU致使沒法繼續調用異步I/O。

瀏覽器中JavaScript與UI公用一個線程,JavaScript長時間執行會致使UI的渲染和響應被中斷。
在Node中,長時間佔用CPU到孩子後續的異步I/O發不出調用,已經完成的異步I/O的回調函數也會得不到執行。

解決:
child_progress:解決單線程中大量算量的問題
Master-Worker:管理各個工做進程 (管理子進程)

啓用一個徹底獨立的進程,將須要計算的程序發送給進程。經過時間將結果傳遞回來。(消息傳遞的方式來傳遞運行結果)

採用消息傳遞的方式: 保持應用模型的簡單和低依賴。

Node的應用場景

  • I/O密集型

    面向網絡且擅長並行I/O,可以有效的組織起更多的硬件資源。
    利用事件循環的處理機制,資源佔用極少。
  • 不是很擅長CPU密集型業務,可是能夠合理調度

    經過編寫C/C++擴展的方式更高效的利用CPU
  • 與遺留系統問題和平共處

    LinkeDin, 雪球財經
  • 分佈式應用

    阿里的數據平臺,對Node的分佈式應用
    分佈式應用要求:對可伸縮性要求高。
    具體應用:
    中間層應用NodeFox,ITer,將數據庫集羣作了劃分和映射,查詢調用一句是針對單張表進行SQL查詢,中間層分解查詢SQL,並行的去多態數據庫中獲取數據併合並。
    NodeFox做用:實現對多臺MySQL數據的查詢
    ITer做用:查詢多個數據庫(指的是不一樣數據庫,MySQL,Oracle等)

Node的使用者

  • 先後端編程語言環境統一:雅虎開放了Cocktail框架

  • Node帶來的高性能的I/O用於實時應用:Voxer和騰訊

    Voxer:實時語音
    騰訊:Node應用在長鏈接,實時功能
  • 並行I/O使得使用者能夠更高效地利用分佈式環境:阿里巴巴和eBay

    利用Node並行I/O的能力,更高校的使用已有的數據
  • 並行I/O,有效利用穩定接口提高Web渲染能力:雪球財經和LinkedIn

  • 雲計算平臺提供Node支持

  • 遊戲開發領域:網易的pomelo實時框架

  • 工具類應用

模塊機制

Node的模塊機制
模塊在引用過程當中的編譯,加載規則
CommonJs規範爲JavaScript提供了一個良好基礎,JavaScript可以在任何地方運行。

CommonJS規範

規範涵蓋:模塊,二進制,Buffer,字符集編碼,I/O流,進程環境,文件系統,套接字,單元測試,Web服務器網管接口,包管理

JavaScript規範缺陷

  • 沒有模塊系統

  • 標準庫較少

  • 沒有標準接口

  • 缺少包管理系統

Node借鑑CommonJS的Modules規範實現了一套很是易用的模塊系統
NPM對Packages規範的無缺支持使得Node在應用開發過程當中更加規範

CommonJS的模塊規範

模塊應用

var math = require('math');

在CommonJs規範中,存在require();方法,這個方法接收模塊標識,以此引入一個模塊的API到當前上下文中。

模塊定義

require(): 引入外部模塊。
exports對象: 導出當前的方法或者變量,並且是惟一導出的出口
module對象:表示自身模塊,而exports是module的屬性。

Node中,一個文件就是一個模塊,將方法掛載到exports對象做爲屬性定義導出方式.

模塊標識

傳遞給require() 方法的參數,必須符合小駝峯命名的字符串,或者以 .,..開頭的相對路徑,或者絕對路徑。 js文件能夠沒有後綴.js

模塊的意義:
將類聚的方法和變量等限定在私有的做用域中,同時支持引入和導出的功能順暢的鏈接上下游依賴。

clipboard.png

Node模塊的實現

Node中引入模塊的步驟:
1:路徑分析
2:文件定位
3:編譯執行

模塊分類

核心模塊:Node提供的模塊
文件模塊:用戶編寫的模塊

核心模塊在Node源代碼的編譯過程當中,編譯進了二進制執行文件。
Node進程啓動時,部分核心模塊就直接被加載近內存中。

文件模塊是在運行時動態加載,須要完成的路徑分析,文件定位,編譯執行的過程。

模塊加載過程

優先從緩存加載

Node對引入過的模塊都是進行緩存,減小二次引入時的開銷。(Node緩存是編譯和執行以後的對象)

不管是核心模塊仍是文件模塊,require()方法對同模塊的二次加載都採用緩存優先的方式。 不一樣之處,核心模塊的緩存檢查優先於文件模塊的緩存檢查

路徑分析和文件定位

○ 模塊標識符分析

  • 核心模塊

  • 路徑形式的文件模塊

  • 自定義模塊

模塊標識符分類:

  • 核心模塊:http,fs,path 等

  • .,..開始的相對路徑的文件模塊

  • /開始的絕對路徑文件模塊

  • 非路徑形式的文件模塊(自定義模塊)

核心模塊
核心模塊的優先級僅此於緩存加載,在Node的源碼編譯過程當中,已經編譯爲二進制代碼,其加載過程最快。

若是想加載和核心模塊標識相同的模塊,必須選擇修改標識或換用路徑的方式。

路徑形式的文件模塊
.,..開始的相對路徑的文件模塊。require();方法會將路徑轉爲真實路徑,並以真實路徑做爲索引,將編譯執行後的結果存放入緩存中。

文件模塊指明瞭確切的文件位置,在查找過程當中節約大量時間,其加載速度慢於核心模塊。

自定義模塊
特殊的文件模塊,多是一個文件或包的形式。這類模塊查找最費時,也是全部方式最慢的一種。

模塊路徑:Node在定位文件模塊的具體文件時定製的查找策略。表現爲一個路徑組成的數組。

node_modules 會按照相似JavaScript的原型鏈查查找方式,在加載過程當中,Node會租個嘗試模塊路徑中的路徑。直到找到文件爲止。(文件路徑越深,模塊查找耗時越多)

clipboard.png

○ 文件定位

  • 文件擴展名分析(.js,.node,.json次序補足擴展名)

  • 目錄分析和包

文件擴展名分析
require(); 在分析標識符的過程當中,標識符不含有文件擴展名的狀況,Node會按照 .js,.node,.json 補足擴展名,以此嘗試

嘗試過程:須要調用fs模塊同步阻塞式判斷文件是否存在。
由於Node是單線程,這邊會引發性能問題。

避免方法:
方法1:若是是.node 和 .json文件,標識符加上擴展名
方法2:同步配合緩存,能夠大幅度緩解Node中阻塞式調用的缺陷

目錄分析和包
在分析文件擴展名以後,沒有找到對應的文件,但卻獲得一個目錄。看成一個包來處理。

Node在當前目錄下查找package.json(CommonJS包規範定義的包描述文件),經過JSON.parse();解析出包描述對象,從中取出main屬性指定的文件名進行定位。若是文件名缺乏擴展名,將會進去擴展名分析的步驟。

若是main屬性指定的文件名錯誤,或者更沒有package.json的文件,Node會將index看成默認文件名,而後一次查找index.js, index.node , index.json

若是在目錄分析的過程當中沒有定位成功任何文件,則自定義模塊進入下一個模塊路徑進行查找。若是模塊路徑數組都遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗的異常。

clipboard.png

○ 編譯模塊

  • .js文件。經過fs模塊同步讀取文件後編譯執行

  • .node文件

    用C/C++編寫的擴展文件
    經過dlopen()方法加載最後編譯生成的文件
  • .json文件

    經過fs模塊同步讀取文件後,
    用JSON.parse()解析返回結果
  • 其它擴展名文件。它們被看成.js文件載入

在Node中,每一個文件模塊都是一個對象。
每個編譯成功的模塊都會將其目錄做爲索引緩存在Module._cache對象上.

JavaScript模塊的編譯
在編譯過程當中,Node對獲取的JavaScript文件內容進行了頭尾包裝。

(function ( exprots, require, moduel, __filename, __dirname ) {  });

包裝以後,對每一個文件之間進行了做用域隔離,包裝以後的代碼會經過vm原生模塊的runIntThisContext();方法執行 (相似eavl,只是具備明確上下文,不污染全局). 返回一個具體的function 對象。 最後,將當前模塊對象的exports屬性,require()方法,module[模塊對象自身],以及在文件定位中獲得的完整文件路徑和文件目錄做爲參數傳遞給這個funciton() 執行。

在執行以後,模塊中的exports 屬性被返回給了調用方。exports屬性上的任何方法和屬性在外界均可以被調用獲得。

有了exports的狀況下,爲什麼還存在module.exports.
給exports從新賦值,exports對象是經過形參的方式傳入的,直接賦值會改變形參的引用,可是不能改變做用域外的值

核心模塊

Node的核心模塊在編譯成可執行文件的過程當中編譯近了二進制文件。

核心模塊:
C/C++編寫,存放在Node項目中src目錄下
JavaScript編寫,存放在lib目錄下

JavaScript核心模塊的編譯過程

  • 轉存爲C/C++代碼

  • 編譯爲JavaScript核心模塊

轉存爲C/C++代碼
Node採用V8的js2c.py工具,將全部內置的JavaScript代碼('src/node.js 和 lib/*.js') 轉換成C++裏的數組,生成node_natives.h頭文件

啓動Node進程時,JavaScript代碼直接加在進內存中。在加載過程當中,JavaScript核心模塊經歷標識符分析後直接定位到內存中,比普通的文件模塊從磁盤從查找快不少。

編譯爲JavaScript核心模塊
JavaScript核心模塊與文件模塊區別:
獲取源代碼的方式(核心模塊是從內存中加載的)以及緩存執行結果的位置。

源文件經過process.bingding('natives'); 取出,編譯成功的模塊緩存到NativeModuel._cache對象上。 文件模塊緩存到Module._cache

function NativeModule(id) {
    this.filename = id + '.js';
    this.id = id;
    this.exports = {};
    this.loaded = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};

C/C++核心模塊的編譯過程
核心模塊中,有些模塊所有有C/C++編寫,有些模塊則由C/C++完成核心部分,其它部分由JavaScript實現包裝或向外處處。

內建模塊: 純C/C++編寫的部分
JavaScript主外實現封裝的模式是Noe可以提升性能的常見方式.
Node的buffer,crypto,evals,fs,os等模塊都是部分經過C/C++編寫 (不直接被用戶調用)

內建模塊的組織形式
每個內建模塊定義以後,都經過NODE_MODULE宏將模塊定義到node命名空間中。

內建模塊的導出
在Node的全部模塊類型中,存在依賴關係。

clipboard.png

通常的,不推薦文件模塊直接調用內建模塊。如需調用,直接調用核心模塊。
由於:核心模塊中基本都封裝了內建模塊。

Node在啓動時,會生成一個全局變量process,並提供Binding()方法謝祖加載內建模塊。
轉爲C/C++數組存儲,經過process.binding('natives'); 取出防止NativeModule.source中.

在加載內建模塊時,先建立exports空對象,而後調用get_builtin_module() 方法取出內建模塊對象,經過執行resister_func()填充exports 對象,最後將exports對象安模塊名緩存,並返回給調用方完成導出。

核心模塊的引入流程
1.NODE_MODULE(node_os,reg_func)
2.get_builtin_module("node_os")
3.process.binding("os")
4.NativeModule.require("os")
5.require("os")

clipboard.png

編寫核心模塊

前提條件:

  • GYP項目生成工具

  • V8引擎C++庫

  • libuv庫

  • Node內部庫

  • 其餘庫,zlib、openssl、http_parser等

C/C++擴展模塊的編寫
C/C++擴展模塊的編譯
C/C++擴展模塊的加載

C/C++ 內建模塊屬於最底層的模塊,屬於核心模塊,主要提供API給JavaScript核心模塊和第三方JavaScript文件模塊調用。

JavaScript核心模塊做用:
1: 做爲C/C++內建模塊的封裝層和橋接層,供文件模塊調用
2: 純粹的功能模塊,不須要跟底層交流,但有很重要。

文件模塊一般由第三方編寫,包括普通JavaScript模塊和C/C++擴展模塊,主要調用方向爲普通JavaScript模塊調用擴展模塊。

clipboard.png

包與NPM

包結構
package.json:包描述文件
bin:用於存放可執行二進制文件的目錄
lib:用於存放JavaScript代碼的目錄
doc:用於存放文檔的目錄
test:用於存放單元測試用例的代碼

包描述文件與NPM
必需字段:name,description,version,keywords,maintainers
必需字段:contributios,bugs,licences,repositories,dependencies

dependencies: 使用當前包所依賴的包列表,NPM會經過這屬性幫助自動加載依賴的包

NPM經常使用功能
查看幫助: npm -v

安裝依賴包
最多見: npm install express
全局安裝:npm install express -g
從本地安裝:npm install <file>
從非官方源安裝:npm install underscore --registry=http://registry.url

NPM鉤子命令
package.json中的script字段:讓包在安轉或者卸載過程當中提供鉤子機制

"scripts": {
    "preinstall": "preinstall.js",
    "install": 'install.js',
    "uninstall": 'uninstall.js',
    "test": "test.js"
}

在執行npm install <package>時, preinstall指向的腳本將會被加載執行,而後install執行的腳本會被執行。在執行npm install <package>時,unstall指向的腳本也許會作一些清理工做。

NPM潛在問題
NPM平臺上面包質量參差不齊
Node代碼能夠運行在服務端,須要考慮安全問題

先後端共用模塊

模塊的側重點
Node的模塊引入過程,幾乎所有都是同步。

AMD規範
AMD規範:是CommonJS模塊規範的一個延伸

//經過數組引入依賴 ,回調函數經過形參傳入依賴
define(['someModule1', ‘someModule2’], function (someModule1, someModule2) {

    function foo () {
        /// someing
        someModule1.test();
    }

    return {foo: foo}
});

CMD規範
CMD規範:玉伯提出,區別定義模塊和依賴引入

//CMD
define(function (requie, exports, module) {
    
    //依賴 就近書寫
    var a = require('./a');
    a.test();
    
    //軟依賴
    if (status) {
    
        var b = requie('./b');
        b.test();
    }
});

1.對於依賴的模塊AMD是提早執行,CMD是延遲執行。RequireJS從2.0開始,也改爲能夠延遲執行(根據寫法不一樣,處理方式不經過)。

2.CMD推崇依賴就近,AMD推崇依賴前置。

UMD規範
兼容多種模塊規範(Universal Module Definition)
UMD判斷是否支持Node.js的模塊(exports)是否存在且module不爲undefiend,存在則使用Node.js模塊模式。
判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。

(function (root, factory) {

    if (typeof define === 'function' && define.amd) {

        // AMD.
        define(['exports'], factory);

    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {

        // CommonJS
        factory(exports, require('echarts'));

    } else if ( typeof module !== 'undefined' ** module.exports ) {

        // 普通Node模塊   
        module.exports = factory();

    } else {
        
        // Browser globals
        factory({}, root);
        
    }
    
}(this, function (exports) {

    //module ...

}));

異步I/O

事件循環是異步實現的核心,與瀏覽器中的執行模型基本保持一致。

古老的Rhino,是較早服務器上運行JavaScript,可是執行模型並不像瀏覽器採用事件驅動.而是使用其它語言同樣採用同步I/O做爲主要模型.

爲何要異步I/O

  • 用戶體驗,消耗時間爲max(M,N)

  • 資源分配,讓單線程遠離阻塞,更好利用CPU

用戶體驗

異步概念: 瀏覽器中JavaScript在單線程上執行,還與UI渲染共用一個線程。 表示JavaScript在執行的時候UI渲染和響應是處於中止狀態

資源分配

單線程同步會由於阻塞I/O致使硬件資源的不到更優使用。多線程編程中的死鎖,狀態同步等問題。

選擇: 利用單線程,原理多線程死鎖,狀態同步等問題,利用異步I/O,讓單線程原理阻塞,更好的使用CPU。

異步I/O實現現狀

操做系統內核對於I/O只有兩種方式:阻塞與非阻塞。
在調用阻塞I/O時,應用程序須要等待I/O纔會返回結果.

阻塞I/O特色:調用以後必定要等到系統內核層面完成全部操做後,調用才結束。

異步I/O與非阻塞I/O
操做系統對計算機進行了抽象,將全部的輸入輸出設備抽象爲文件。內核在進行文件I/O操做時,經過文件描述符進行管理。
應用程序若是須要進行I/O調用,須要先打開文件描述符,而後再更具文件描述符去實現文件的數據讀寫。
阻塞I/O: 完成整個獲取數據的過程。
非阻塞I/O: 不帶數據直接返回,要獲取數據,還須要經過文件描述符再次讀取。

爲了獲取完整數據,應用程序須要重複調用I/O操做來確認是否完成。這種重複調用判斷操做是否完成的叫作輪詢;

輪詢: CPU決策如何提供周邊設備服務的方式。 又稱爲 「程控輸出入」 (Programmed I/O)。
輪詢法: 由CPU定時發出詢問,依次詢問每個周邊設備是否須要其服務,有即給予服務。服務結束再詢問下一個周邊,重複詢問。

非阻塞I/O缺點:輪詢去確認是否徹底完成數據獲取,會讓CPU處理狀態判斷,是對CPU資源的浪費。須要減少I/O狀態判斷的CPU損耗.

輪詢技術

  • read

  • select

  • poll

  • epoll

  • kqueue

read : 重複調用來檢查I/O的狀態來完成完整數據的讀取。
缺點:性能最低

clipboard.png

select : 文件描述符上的事件狀態來進行判斷
缺點:最多可同時檢查1024個文件描述符.

clipboard.png

poll : 鏈表的方式避免數組長度的限制,能避免不須要的檢查
缺點:文件描述符較多,性能低下。

clipboard.png

epoll : I/O事件通知機制。
進入輪詢的時候若是沒有檢測到I/O事件,將會進行休眠,知道事件發生將它喚醒。
事件通知,執行回調的方式。而不是遍歷查詢。
特色:不會浪費CPU,執行效率較高。

clipboard.png

kqueue : 實現方式和epoll相似,存在FreeBSD系統下。

輪詢缺點:
輪詢對於應用程序,仍然是一種同步,應用程序讓然須要等待I/O徹底返回,依舊花費不少時間來等待。等待期間CPU要麼用於遍歷文件描述符的狀態,要麼用戶休眠等待事件發生。

理想的非阻塞異步I/O

應用程序發起非阻塞調用,無需經過遍歷或者事件喚醒等方式輪詢,能夠直接處理下一個任務。
在I/O完成後經過信號或回調將數據傳遞給應用程序。

clipboard.png

現實中的異步I/O

*nix平臺下采用libeio配合libev實現I/O部分
windows平臺採用IOCP是實現異步I/O

部分線程阻塞I/O 或者 非阻塞I/O + 輪詢技術 -> 完成數據獲取。
一個線程計算處理
經過線程之間的通訊將I/O獲得的數據進行傳遞。

clipboard.png

IOCP: 調用異步方法,等待I/O完成以後的通知,執行回調,用戶無序考慮輪詢。(實現原理:線程池原理)

*nix將計算機抽象:磁盤文件,硬件,套接字,等幾乎全部計算機資源抽象爲文件。

在Node中,不管是*nix仍是Windows平臺,內部完成I/O任務的另有線程池。

Node的異步I/O

  • 事件循環

  • 觀察者

  • 請求對象

  • 執行回調

事件循環觀察者請求對象I/O線程池共同構成Node異步I/O模型的基本要素。

事件循環
Node 自身的執行模型 -- 事件循環。
在進程啓動時,Node會穿件一個相似於while(true)的循環,每執行一次循環體的過程 稱之爲: Tick(標記,打勾) .每一個Tick的過程就是查看是否有事件待處理,若是有,就取出事件及其相關的回調函數。若是存在關聯的回調,就執行它們。而後進入下個循環,若是再也不有事件處理,就退出進程。

clipboard.png

觀察者

每一個事件循環中有一個或者多個觀察者,而判斷是否有事件要處理的過程就是想這些觀察者詢問是否有要處理的事件。

瀏覽器相似事件觀察機制: 時間可能來自用戶的點擊或者加載某些文件時產生,而這些產生的事件都有對應的觀察者。

事件循環是一個典型的生產者/消費者模型,$watch $digest 機制。 異步I/O,網絡請求則是事件的生產者,遠遠不斷爲Node提供不一樣類型的事件,這些事件被傳遞到對應的觀察者那裏,事件循環則從觀察則那裏取出事件並處理。

請求對象

請求對象: 從JavaScript 發起調用到內核執行完I/O操做的過分過程當中,存在一種中間產物.

請求對象是異步I/O過程當中的重要中間產物,全部的狀態都保存在這對象中,包括送入線程池等待執行以及I/O操做完畢後的回調處理。

Node中的異步I/O調用,回調函數不禁開發者來調用。
從發出調用後,到回調函數被執行,中間發生了什麼?

fs.open() 示例:

fs.open = function ( path, flags, mode, cb ) {
    
    binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, cb);    

};

clipboard.png

Node經典調用方式:從JavaScript調用Node的核心模塊,核心模塊調用C++內建模塊,內建模塊經過libuv進行系統調用。

調用的uv_fs_open() -> FsReqWrap (請求對象,做用:JavaScript層傳入的參數和當前方法都封裝對象中)

回調函數被設置在FsReqWrap.oncomlete_sym 屬性上

req_wrap -> object_ -> Set(oncomlete_sym, callback);

包裝完以後,Windows調用:QueueUserWorkItem() 將FsReqWrap對象推入線程池中等待執行

QueueUserWrokITem(&uv_fs_thread_proc, req, WT_EXTCUTEDEFAULT);

QueueUserWorkItem()
參數1:執行的方法引用 fs_open() 的引用 uv_fs_thread_proc
參數2:uv_fs_thread_proc() 方法執行時所需參數。
參數3:執行的標誌。

調用完成以後,JavaScript調用當即返回,由JavaScript層面發起的異步調用的第一階段結束。
JavaScript線程能夠繼續執行當前任務的後續操做。當前的I/O操做在線程池中等待執行,無論是否阻塞I/O,都不會影響到JavaScript線程的後續執行。

執行回調

回調通知,完成完整異步I/O的第二部分。

線程池中的I/O操做調用完畢後,會將結果存儲在req -> reslut 屬性上。而後調用 PostQueuedCompletionStatus(); 通知IOCP,告知當前對象操做已經完成:

PostQueuedCompletionStatus( (loop)->iocp, o, o, &(req)->overlappped )

PostQueuedCompletionStatus()做用: 向IOCP提交執行狀態,並將線程歸還給線程池。提交狀態
GetQueuedCompletionStatus() 做用: 提取

每一個Tick的執行中,會調用IOCP相關的GetQueuedCompletionStatus()方法檢查線程池中是否有執行完的請求,若是存在,會將請求加入到I/O觀察者的隊列中,而後將其看成事件處理。

I/O觀察者回調函數的行爲: 取出請求對象的result屬性做爲參數,取出oncomlplete_sym屬性做爲方法,而後調用執行。以此達到調用JavaScript中傳入的回調函數的目的。

clipboard.png

非I/O的異步API

定時器
定時器: setTimeout(),setInterval()
調用setTimeout()或者setInterval()建立的定時器會被插入到定時器觀察者內部的一個紅黑樹(做用:實現關聯數組)中
每次Tick執行時,會從該紅黑樹中迭代取出定時器對象,檢查是否超過定時時間,若是超過,就造成一個事件,它的回調函數將當即執行。

定時器問題:並不是精確的(在偏差範圍內)。若是某一次循環佔用的時間較多。那麼下次循環時,也許超時好久。
例如:setTImeout()設定一個任務在10毫秒後執行,可是9毫秒後,有一個任務佔用了5毫秒的CPU時間片,再次輪到定時器時,時間就已通過期4毫秒。

clipboard.png

process.nextTick()

process.nextTick(): 操做比較輕量,高效

會將回調函數放入隊列中,在下一輪Tick時取出執行,每輪循環中會將數組中的回調函數所有執行完。

setImmediate()
setImmediate()與上者相似,但優先級低於process.nextTick()
緣由:事件循環對觀察者的檢查是有前後順序。 process.nextTick(); 屬於idle觀察者,setImmediate() 屬於check觀察者。
在每一輪循環檢查中,idle觀察者I/O觀察者,I/O觀察者先於check觀察者

會將結果保存在鏈表中。每輪循環中執行鏈表中的一個回調函數。

process.nextTick(function () {
    console.log('nextTick延遲執行');
});
setImmediate(function () {
    console.log('setImmediate延遲執行');
});
console.log('正常執行');

// 執行結果
// 正常執行
// nextTick延遲執行
// setImmediate延遲執行

事件驅動和高性能服務器

事件驅動的本質:經過主循環加事件觸發的方式來運行程序。

經典模型

clipboard.png

  • 同步式

    一次只能處理一個請求,而且其他請求都處於等待狀態。
  • 每進程/每請求

    每一個請求啓動一個進程,這樣能夠處理多個請求。
    缺點:不具有擴展性。(由於系統資源有限)
  • 每線程/每請求 (Apache)

    爲每一個請求啓動一個線程來處理。
    優勢:線程比進程要輕量
    缺點:每一個線程都佔用必定內存,當大併發請求到來時,內存會很快耗光,致使服務器緩慢。

    線程(程序執行流的最小單元)

進程(一段程序的執行過程)
一個進程中包括多個線程

異步編程

函數式編程

高階函數

高階函數:把函數做爲輸入或返回。做爲參數或者返回值的函數

偏函數用法:建立一個調用另一部分——參數或變量已經預置的函數——的函數的用法。
經過指定部分參數來建立一個新的函數的形式。

異步編程的優點與難點

優點
優點:基於事件驅動的非阻塞I/O模型
效果:非阻塞I/O能夠是CPU與I/O並不相互依賴等待,讓資源獲得更好的利用。

難點

  • 異常處理

    異步I/O的實現主要包含兩個階段:提交請求和處理結果。
    這兩個階段中間有事件循環的調度,二者彼此不關聯。
    異步方法則一般在第一階段提交請求後當即返回,由於異常並不發生在這個階段。
  • 函數嵌套過深

  • 阻塞代碼

  • 多線程編程

  • 異步轉同步

異步編程解決方案

事件發佈/訂閱模式
事件監聽器模式:回調函數的事件化,又稱發佈/訂閱模式 (鉤子(hook)機制)

Node中的不少對象大多具備黑盒的特色,功能點較少。

事件發佈/訂閱模式自身並沒有同步和異步調用的問題。但在Node中,emit()調用多半是伴隨着時間循環而異步觸發。

若是對一個時間添加了超過10個偵聽器,將會獲得一條警告。(緣由:偵聽器太多可能致使內存泄漏)。能夠經過 emitter.setMaxListeners(0); 去掉這個限制。
因爲事件發佈會引發一些列偵聽器執行,若是事件相關的偵聽器過多,可能存在過多佔用CPU
的情形。

繼承events模塊

util.inherits(constructor, superConstructor)
繼承原型對象上的方法。

var events = require('events');
var util = require('util');

function Stream () {
    events.EventEmitter.call(this);
}

util.inherits(Stream, events.EventEmitter);

利用事件隊列解決雪崩問題

事件訂閱/發佈模式中,一般由一個once()方法。
做用:偵聽器只能執行一次,在執行以後就會將它與事件的關聯解除。
解決:過濾一些重複性的事件響應。

問題:緩存中存放在內存中,訪問速度十分快,用於加速數據訪問,讓絕大所數的請求沒必要重複去作一些抵消的數據讀取。
雪崩問題:高訪問量,大併發量的狀況下緩存失效的情景,此時大量的請求同時涌入數據庫中,數據庫沒法同時承受如此大的查詢請求。

解決方案:添加一個狀態鎖。

var proxy = new evnets.EventEmitter();
var status = 'ready';
var select = function ( cb ) {
    proxy.once('selected', cb);
    if ( status === 'ready' ) {
        status = 'pending';
        db.select('SQL', function ( results ) {
            proxy.emit('selected', results);    
            status = 'ready';
        });
    }
}
// 利用once() 方法,將全部請求的回調都壓入事件隊列中。保證回只會被執行一次。

Gearman異步應用框架中,利用noce()方法產生的效果。

多異步之間的協做方案

事件與偵聽器的關係是一對多,在異步編程中,會出現事件與偵聽器的關係多對一的狀況。
一個業務邏輯可能依賴兩個經過回調或事件傳遞的結果。

問題:多個異步場景中的回調函數的執行順序,且回調之間沒有任何交集。(多對一)
解決辦法:經過第三方函數和第三方變量來處理異步協做 -- 哨兵變量。

var after = function ( times, cb ) {
    
    var count = 0, resluts = {};
    
    return function ( key, value) {
        
        resluts[key] = value;
        
        count++;
        
        if ( coutn === times ) {
            
            cb(resluts);
            
        }
        
    }
    
}

哨兵變量和發佈/訂閱模式完成多對多方案

// 哨兵變量
var after = function ( times, cb ) {
    
    var count = 0, resluts = {};
    
    return function ( key, value) {
        
        resluts[key] = value;
        
        count++;
        
        if ( coutn === times ) {
            
            cb(resluts);
            
        }
        
    }
    
}

// 哨兵變量和發佈/訂閱模式完成多對多方案

var events = require('evnets');

var emitter = new events.Emitter();

var done = after(times, render);

emitter.on('done', done);
emitter.on('done', other);

fs.readFile(template_path, 'utf-8', function ( err, template ) {
    
    emitter.emit('done', 'template', template);
    
});

db.query(sql, function ( err, data ) {
    
    emitter.emit('done', 'data', data);
    
});


l1on.get(function ( err, resources ) {
    
    emitter.emit('done', 'resources', resources);
    
});

EventProxy模塊

all();來訂閱多個事件,當每一個時間都被觸發後,偵聽器纔會執行。
tail(); 偵聽器在知足條件以後只會執行一次。

場景:從一個接口屢次讀取數據,此時觸發的事件名或許是相同的。

利用after();方法實現時間在執行多少次後執行偵聽器的單一事件組合訂閱方式。

var proxy = new EventProxy();

proxy.after('data', 10, function ( datas ) {
    // TODO
});

EventProxy原理

每一個非all事件出發時都會觸發一次all事件。

EventProxy是將all看成一個事件流的攔截層,在其中注入一些業務來處理單一時間沒法解決的異步問題。

EventProxy的異常處理

exports.getCountent = function ( cb ) {
    
    var ep = new EventProxy();
    
    ep.all('tpl', 'data', function ( tpl ,data ) {
        
        // 成功回調
        cb(null, {
            template: tpl,
            data: data
        });
        
    });
    
    // 綁定錯誤處理函數
    ep.fail(cb);
    
    fs.readFile('template.tpl', 'utf-8', ep.done('tpl'));
    
    db.get('some sql', ep.done('data'));
    
}

// EventProxy模塊提供fail()和done();實例方法來優化異常處理。

Promise/Deferred模式

使用發佈訂閱模式缺點:使用事件的方式,執行流程須要被預先設定。

場景:先執行異步調用,延遲傳遞處理的方式。

Promises/A

Promises/A:對單個異步操做抽象定義

  • Promise 操做只會處在3中狀態:未完成態,完成態和失敗態。

  • Promise 的狀態只會出現從未完成態向完成態或失敗態轉化,不能逆反。完成態和失敗態不能互相轉化。
    Promise的狀態一旦轉化,將不能被更改。

Promises/A要求Promise對象只需具有then()方法便可。

  • 接受完成態,錯誤態的回調方法,在操做完成或出現錯誤時,將會調用對應方法。

  • 可選擇支持progress事件回調做爲第三個方法。

  • then()方法之接受function對象,其他對象將被忽略。

  • then()方法繼續返回Promise對象,實現鏈式調用。

    then(fulfilledHandler, errorHandler, progressHandler);

Node的events模塊來完成Promise對象的then()方法

var util = require('util');
var events = require('events');

var Promise = function () {
    
    evennts.EventEmitter.call(this);
    
}

util.inherits(Promise, EventEmitter);

Promise.prototype.then = function ( fulfilledHandler, errorHandler, progressHandler ) {
    
    // 完成態
    if ( typeof fulfilledHandler === 'function' ) {
        
        this.once('success', fulfilledHandler);
        
    }
    
    // 失敗態
    if ( typeof errorHandler === 'function' ) {
        
        this.once('error', errorHandler);
        
    }
    
    
    // 執行
    if ( typeof progressHandler === 'function' ) {
        
        this.on('progress', progressHandler);
        
    }
    return this;
}

Deferred 延遲對象:觸發 Promise對象中知足條件的函數,並實現這些功能。

// Deferred
var Deferred = function () {
    
    this.state = 'unfulfilled';
    this.promise = new Promise();
    
}

// 實現完成態
Deferred.prototype.resolve = function ( obj ) {
    
    this.state = 'fulfilled';
    this.promise.emit('success', obj);
    
}

// 實現失敗態
Deferred.prototype.reject = function ( err ) {
    
    this.state = 'failed';
    this.promise.emit('error', err);
    
}

Deferred.prototype.progress = function ( data ) {
    
    this.promise.emit('progress', data);
    
}

Deferred主要用於內部,用於維護異步模型的狀態
Promise做用於外部,經過then()方法暴露給外部以添加自定義邏輯。
Promise/Deferred -- 響應對象

clipboard.png

高級接口:不容易變化,再也不有低級接口的靈活性。
低級接口:能夠構成更多更負責的場景。

Q模塊是Promises/A規範的一個實現

defer.prototype.makeNodeResolver = function () {
    
    var self = this;
    
    return function ( error, value ) {
        
        if ( error ) {
            
            self.reject(error);
            
        } else if ( arguments.length > 2 ) {
            
            self.resolve(array_slice(arguments,1));
            
        } else {
            
            self.resolve(value);
            
        }
        
    }

}

項目中使用:Q模塊,和when來解決異步操做問題。它們是完整的Promise提議的實現。

var fs = require('fs');
var Q = require('Q');

var readFile = function ( file , encoding ) {
    
    var deferred = Q.defer();
    fs.readFile(file, encoding, deferred.makeNodeResolver());
    
    return deferred.promise; 
    
}

readFile('foo.txt', 'utf-8').then(function ( data ) {
    
    // success
    console.log( data );
    
}, function ( err ) {
    
    // error
    console.log('error:' , err);
    
});

Promise中多異步協議

Promise解決的是:單個異步操做中存在的問題.

全部操做成功,這個異步操做才成功,一旦其中一個異步操做失敗,整個一部操做就失敗。

Promise的進階知識

Promise缺點:須要爲不一樣的場景封裝不一樣的API,沒有直接的原生事件那麼靈活。

Promise最主要的是處理隊列操做。
支持序列執行的Promise

promise()
    .then(obj.api1)
    .then(obj.api2)
    .then(obj.api3)
    .then(function ( valu4 ) {
             // Do something with value4
    }, function ( error ) {

    })
    .done();

Promise支持鏈式執行步驟:
1:將全部的回調都存到隊列中
2:Promise完成時,租個執行回調,一旦檢測返回了新的Promise對象,中止執行,而後將當前Deferred對象的promise引用改變爲新的Promise對象,並將隊列中的餘下的回調轉交給它。

流程控制庫

尾觸發與Next

尾觸發:手動調用才能持續執行後續調用,關鍵字next
應用:Connect中間件中.Connect中間件傳遞請求對象,響應對象和尾觸發函數,經過隊列造成一個處理流。

// Connect的核心實現
function createServer () {
    
    function app ( req, res ) {
        app.handle(req,res);
    }
    
    utils.merge(app, proto);
    utils.merge(app, EventEmitter.prototype);
    
    app.route = '/';
    app.stack = []; // 核心代碼
    // stack屬性時服務器內部維護的中間件隊列
    
    for ( var i=0; i<arguments.length; ++i ) {
        app.use(arguments[i]); //  調用use()能夠 將中間件放入隊列中
    }
    
    return app;
    
}

中間件這種尾觸發模式並非要求每一箇中間方法都是異步,但若是每步驟都採用異步來完成,其實是串行化的處理。
流式處理將一些串行的邏輯扁平化。

串行化:將對象存儲到介質(如文件,內存緩衝區等)中或是以二進制方式經過網絡傳輸。而後須要使用的時候經過:反串行化從這些連續的字節數據從新構建一個與原始對象狀態相同的對象。
做用:有時候,須要將對象的狀態保存下來,在須要時再會將對象恢復。
對象經過寫出描述本身的狀態數值來記錄本身,這個過程叫對象串行化。

async

流程控制模塊async

○異步的串行執行

series(); 實現一組任務的串行執行

var fs = require('fs');
var async = require('async');

// 異步串行執行
async.series([function (cb) {
    
    fs.readFile('foo.txt', 'utf-8', cb);
    
},function ( cb ) {
    
    fs.readFile('foo2.txt', 'utf-8', cb);
    
}],function ( err, resluts ) {
    
    console.log( resluts );
    
});

// series(); 方法中傳入的函數cb();並不是有使用者指定。此處的回調函數有async經過高階函數的方式注入。每一個cb();執行時會將結果保存起來,而後執行下一個調用,知道結束全部調用。最終的回調函數執行時,隊列裏的異步調用保存的結果以數組的方式傳入。
// 異常處理規則:一旦出現異常,就結束全部調用,並將異常傳遞給最終回調函數的第一個參數。

○異步的並行執行
並行做用:提高性能

parallel(); 以並行執行一些異步操做。

var fs = require('fs');
var async = require('async');

// 異步的並行執行
async.parallel([function ( cb ) {
    
    fs.readFile('foo.txt', 'utf-8', cb);
    
},function ( cb ) {
    
    fs.readFile('foo2.txt', 'utf-8', cb);
    
}],function ( err, reslut ) {
    
    console.log( reslut );
    
});
// 經過注入的回調函數。
// parallel(); 對於異常的判斷依然是一旦某個異步調用產生了異常,就會將異常做爲第一個參數傳遞給最終的回調函數。

○異步調用的依賴處理
series(); 適合無以來的異步串行。
缺點:當前一個的結果是後一個調用的輸入時。
使用:waterfall();

var fs = require('fs');
var async = require('async');

async.waterfall([function ( cb ) {
    
    fs.readFile('foo.txt', 'utf-8', function ( err, content ) {
        
        cb(err, content);
        
    });
    
},function ( arg1, cb ) {
    
    fs.readFile('foo2.txt', 'utf-8', function ( err, content ) {
        
        cb(err, content + arg1);
        
    });
    
}],function ( err, resluts ) {
    
    console.log( resluts );
    
});

○自動依賴處理
auto(); 根據依賴自動分析,以最佳的順序執行業務。
實現複雜的依賴關係,業務中或是異步,或是同步。

var deps = {
    readConfig: function ( cb ) {
        cb();
    },
    connectMongoDB: ['readConfig', function ( cb ) {
        cb();
    }],
    connectRedis: ['readConfig', function ( cb ) {
        cb();
    }],
    complieAsserts: function ( cb ) {
        cb();    
    },
    uploadAsserts: ['complieAsserts', function (cb ) {
        cb();
    }],
    startup: ['connectMongoDB', 'connectRedis', 'uploadAsserts', function ( cb ) {
        // startup
    }]
}

async.auto(deps);

Step

Step只有一個接口Step.
Step(task1, task2, task3);
Step接受任意數量的任務,全部任務都將會串行依次執行。

var Step = require('step');
var fs = require('fs');

Step(function readFile1() {
    
    fs.readFile('foo.txt', 'utf-8', this);
    
}, function readFile2( err, content ) {
    
    fs.readFile('foo2.txt', 'utf-8', this);
    
}, function done( err, content ) {
    
    console.log( content );
    
});

// Step使用到了this關鍵字,它是Step內部的一個next()方法,將異步調用的結果傳遞給下一個任務做爲參數,並調用執行

○並行任務執行
parael();方法,告知Step須要等到全部任務完時才進行下一個任務。

var Step = require('step');
var fs = require('fs');

Step(function readFile1() {
    
    fs.readFile('foo.txt', 'utf-8', this.parallel());
    fs.readFile('foo2.txt', 'utf-8', this.parallel());
    
},function done( err, content1, content2 ) {
    
    console.log( arguments );
    
});
// 異常處理:一旦有一個異常產生,這個異常會做爲下一個方法的第一個參數傳入。

○結果分組
this.group();
將返回的數據保存在數組中

異步併發控制

緣由:併發量過大,下層服務器將會吃不消。若是對文件系統進行大量併發調用,操做系統的文件描述符數量將會被瞬間用光。

bagpipe的解決方案

  • 經過一個隊列來控制併發量

  • 若是當前活躍(指調用發起但爲執行回調)的異步調用量小於限定值,從隊列中取出執行。

  • 若是活躍調用達到限定值,調用暫時存放在隊列中。

  • 每一個異步調用結束時,從隊列中取出心兒異步調用執行。

bagpipe中的push();和full事件
var Bagpipe = require('bagpipe');

// 設定最大併發數爲10
var bagpipe = new Bagpipe(10);

for ( var i=0; i<100; i++ ) {
    
    bagpipe.push(async, function () {
        // 異步回調執行
    });
    
}

bagpipe.on('full', function ( length ) {
    
    console.warn('底層系統處理不能及時完成,隊列擁堵,目前隊列長度爲:'+ length);
    
});

拒絕模式
做用:大量異步調用須要分場景,須要實時方面就快速返回。
在設定併發數時,參數設置爲true

// 設定最大併發數爲10
var bagpipe = new Bagpipe(10, {
    refuse: true
});

超時控制
緣由:異步調用耗時過久,調用產生的速度遠遠高於執行的速度。防止某些異步調用使用太多的時間。
實現:將執行時間過久的異步調用清理出活躍隊列。經過,設定時間閥值。
效果:排隊中的異步調用越快執行。

var bagpipe = new Bagpipe(10, {
    timeout: 3000
});

async的解決方案
parallelLimit(); 處理異步調用的限制
缺點:沒法動態增長並行任務。

var fs = require('fs');
var async = require('async');

async.parallelLimit([function ( cb ) {
    
    fs.readFile('foo.txt', 'utf-8', cb);
    
},function ( cb ) {
    
    fs.readFile('foo2.txt', 'utf-8', cb);
    
}], 1, function ( err, resluts ) { // 用於限制併發數量的參數,任務只能同時併發必定數量,而不是無限制併發
    
    console.log( resluts );
    
});

queue(); 處理異步調用限制,可以的動態增長任務。
通常用於:遍歷文件目錄等操做

var q = async.queue(function ( file, cb ) {
    fs.readFile('foo.txt', 'utf-8', cb);
}, 2);

q.drain = function () {
    
    // 完成對了中的全部任務
    
}

fs.readdirSync('.').forEach(function ( file ) {
    
    q.push(file, function ( err, data ) {
        
        // TODO
        
    });
    
});

內存控制

V8的垃圾回收機制與內存限制

JavaScript的垃圾回收機制是自動進行內存管理。

V8的內存限制

Node中經過JavaScript使用內存時只能使用部份內存(64位系統下約1.4GB,32位系統下約爲0.7GB)
限制了沒法操做大內存對象。

Node中使用的JavaScript對象基本上都是經過V8自身的方式來進行分配和管理。

V8爲什麼限制了內存使用量,須要迴歸到V8在內存使用上的策略。

V8的對象分配

在V8中,全部的JavaScript對象都是經過堆來進行分配的。

查看內存信息:

var usage = process.memoryUsage();

console.log( usage );
// { rss: 17170432, heapTotal: 8384512, heapUsed: 3787248 }
// rss  headTotal 已經申請到堆內存,   heapUsed 當前使用量 // 單位:bytes

當代碼中聲明變量並賦值時,所使用對象的內存就分配在堆中。若是已經申請的堆空想內存不夠分配新的對象,將繼續申請堆內存,直到堆的代銷超過V8的限制爲止。

V8限制堆大小緣由:

  • V8最初爲瀏覽器而設計,不太可能使用到大量內存的場景。

  • V8垃圾回收機制的限制。

    1.5GB的垃圾回收堆內存爲例,V8作一次小的垃圾回收須要50毫秒以上,作一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引發JavaScript線程暫停執行的時間,在這樣的時間花銷下,應用的性能和響應能力都會直線降低。考慮直接限制堆內存。

    Node能夠在啓動調節內存限制大小

node --max-old-spce-size=1700 test.js // 單位MB
node --max-new-spce-size=1024 test.js // 單位 KB
//  一旦生效就不能再動態改變。

V8的垃圾回收機制

1:V8主要的垃圾回收算法
V8垃圾回收策略主要基於分代式垃圾回收機制。
實際應用中,對象的生存週期長短不一,不一樣的算法只能針對特定狀況具備最好的效果。

V8的內存分代

在V8中,主要講內存分爲新生代和老生代。
新生代:對象爲存活時間較短對象。 --max-new-space-size命令行參數設置 新生代內存空間大小(最大值:64位操做系統32MB,32位操做系統16MB)
老生代:對象爲存活較長或常駐內存的對象。--max-old-spce-size命令行參數設置 老生代內存空間大小。(64位操做系統下:1400MB,32位系統700MB)

使用缺陷:在啓動Node時就指定,沒法根據V8內存使用狀況自動擴充。

默認狀況下,V8堆內存最大值:64位操做系統:1464MB,32位操做系統732MB。(4 * reserved_semispce_size + max_old_generation_size_)

○ Scavenge算法
新生代中的對象主要經過Scavenge算法進行垃圾回收。
Scavenge採用了:Cheney算法。

Cheney算法:複製的方式實現的垃圾回收算法。
將堆內存一分爲二,每一部分空間成爲semispce。這兩個semispce空間中,只有一個處於試用,另外一個處於閒置狀態。處於使用狀態的semispce空間稱成爲From空間。處於限制狀態的空間成爲To空間。

分配原則:分配對象時,顯示在Form空間中進行分配,當開始進行垃圾回收時,會先檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將被釋放。完成賦值後,Form空間和To空間的角色發生對換。

垃圾回收的過程當中,就是經過將存活對象在兩個semispce空間之間進行復制。

Scavenge缺點:只能使用堆內存中的一半內存。
優勢:時間效率高 (犧牲空間換取時間)
合適於新生代對象中,由於:新生代中的對象的生命週期比較短。

當一個對象進過屢次賦值依然存活時,它將會被認爲是生命週期較長的對象。會被轉移到老生代中。採用新的算法進行管理。

From空間中的存活對象在複製到TO空間以前須要進行檢查。在必定條件下,須要將存活週期長的對象移動到老生代中,也就是完成對象晉升。

晉升條件:

  • 經歷一次Scavenge回收

    在默認狀況下,V8的對象分配主要集中在From空間中,對象從From空間中複製到To空間時,會檢查它的內存地址來判斷這個對象是否已經經歷過一次Scavenge回收。若是經歷過了,就將該對象從From空間複製到老生代空間中,若是沒有,則複製到TO空間中。
  • To空間的內存佔用比。

    當要從From空間複製一個對象到To空間時,若是To空間已經使用超過25%,則這個對象直接晉升到老生代空間中。(25%限制值的緣由,當這個次Scavenge回收完成後,To空間將會變成From空間,接下來的內存分配將在這個空間中進行。)

○ Mark-Sweep & Mark-Compact

存活對象佔用較大比重,採用Scavenge方式產生問題:
1:存活對象較多,複製存活對象的效率將會很低
2: 浪費通常空間的問題。

Mark-Sweep標記清除:標記和清除,兩個階段。
Mark-Sweep在標記階段遍歷堆中全部對象,並標記着活着的對象。在隨後的清除階段只清除沒有被標記的對象。
產生問題:進行一次標記清除回收後,內存會出現不連續的狀態。
緣由:內存碎片會對後續的內存分配形成問題,極可能出現須要分配一個大對象的狀況,全部的碎片空間沒法完成這次分配。就會提早出發垃圾回收。

Mark-Compact 標記整理
解決:Mark-Sweep內存碎片問題。
對象標記死亡後,在整理的過程當中,將或者對象往一端移動,移動完成後,直接清理邊界的內存。
缺點:速度慢。

V8主要使用Mark-Sweep,在空間不足以對重新生代中晉升過來的對象進行分配時才使用Mark-Compact

○ Incremental Marking (增量標記)
全停頓:將應用邏輯展亭下來,待執行完來回收後再回復執行應用邏輯。

爲了不出現JavaScript應用邏輯與垃圾回收器看到的不一致的狀況。三種垃圾回收機制都採用全停頓。

解決辦法:增量標記
垃圾回收與應用邏輯交替執行直到標記階段完成。
除了增量標記,V8還引入延遲清理,增量式清理。
讓清理與整理動過也變成增量式。

V8對內存限制的設置對於Chrome瀏覽器這種每一個選項卡頁面使用一個V8實例而言,內存的使用時綽綽有餘。
Node編寫服務端來講,V8垃圾回收特色和JavaScript在單線程上的執行狀況,垃圾回收時影響性能的因素之一。
高新能的執行效率,須要主要讓垃圾回收儘可能少的進行,尤爲是全堆垃圾回收。

○ 查看垃圾回收日誌

node --tarce_gc -e "var a = []; for (var i=0; i<1000000; i++) a.psuh(new Array(100))" > gc.log
// 垃圾回收日誌信息

node --prof test.js
// 垃圾回收時所佔用的時間

高效使用內存

做用域

在JavaScript中能造成做用域的有函數,width以及全局做用域。

var foo = function () {
    var local = {};
}

內存回收過程
foo(); 函數在每次被調用時會建立對應的做用域,函數執行結束後,該做用域將會銷燬。
同時做用域中聲明的局部變量分配在該做用域上,隨着做用域的銷燬而銷燬。只被局部變量引用的對象存活週期較短。
因爲對象很是小,將會分配在新生代的From空間中,在做用域釋放後,局部變量的elocal失效,其引用的對象將會在下次垃圾回收時被釋放。

  • 標識符查找

    與做用域相關的便是標識符查找。
  • 做用域鏈

  • 變量的主動釋放。

    若是變量是全局變量,(不經過var聲明或定義在global變量上),因爲全局做用域須要直到進程退出才能釋放,致使引用的對象常駐內存(常駐在老生代中)。若是須要釋放常駐內存的對象,能夠經過delete操做符刪除引用關係。或者將變量從新賦值,讓舊的對象脫離引用關係。
    在非全局做用域中,想主動釋放變量引用的對象,也能夠經過delete和從新賦值。可是V8中經過delete刪除對象的屬性有可能干擾V8的優化,經過賦值方式解除引用更好。

閉包

做用域鏈上的對象訪問只能向上,外部沒法向內部訪問。
閉包:實現外部做用域訪問內部做用域的中的變量的方法。
高階函數特性:函數能夠做爲參數或者返回值。

閉包問題:一旦有變量應用這個中間函數,這個中間函數將不會釋放,同時也會使原始的做用域不會獲得釋放,做用域中產生的內存佔用也不會獲得釋放。

相關文章
相關標籤/搜索