摘要:簡介項目命名為就是一個服務器單純開發(fā)一個服務器的想法,變成構建網(wǎng)絡應用的一個基本框架發(fā)展為一個強制不共享任何資源的單線程,單進程系統(tǒng)。單線程弱點無法利用多核錯誤會引起整個應用退出,應用的健壯性大量計算占用導致無法繼續(xù)調用異步。
NodeJs簡介
Ryan Dahl項目命名為:web.js 就是一個Web服務器.
單純開發(fā)一個Web服務器的想法,變成構建網(wǎng)絡應用的一個基本框架.
Node發(fā)展為一個強制不共享任何資源的單線程,單進程系統(tǒng)。
每一個Node進程都構成這個網(wǎng)絡應用中的一個節(jié)點,這是它名字所含意義的真諦。
2009年3月,Ryan Dahl在博客宣布并創(chuàng)建
2009年5月,在GitHub上發(fā)布最初的版本
2009年12月和2010年4月,兩屆JSConf大會安排了Node的講座
2010年底,Ryan Dahl加入Joyent全職負責Node的發(fā)展
2011年7月,發(fā)布Windows版本
2011年11月,成為GitHub上面關注度最高的項目
2012年1月底,Ryan Dahl 將掌門人身份交給NPM的作者Issac Z.Schlueter
2013年7月,發(fā)布穩(wěn)定版V0.10.13
隨后,Node的發(fā)布計劃主要集中在性能上面,V0.14后正式發(fā)布V1.0版本
選擇JavaScript高性能(chrome的V8引擎的高性能)
符合事件驅動(JavaScript在瀏覽器中有廣泛的事件驅動方面的應用)
沒有歷史包袱(為其導入非阻塞的I/O庫沒有而外阻力)
Node給JS帶來的意義Node結構與Chrome十分相似,基于事件驅動的異步架構
Node中JS可以訪問本地文件,搭建服務器,連接數(shù)據(jù)庫
Node打破了過去JS只能在瀏覽器中運行的局面,前后端編程環(huán)境統(tǒng)一
Node特點異步I/O
事件與回調函數(shù)
單線程* child_progress:解決單線程中大量算量的問題 * Master-Worker:管理各個工作進程跨平臺:兼容Windows和*nix平臺
構建異步I/O,從文件讀取到網(wǎng)絡請求。
可以從語言層面很自然的進行并行I/O 操作。每個調用之間無序等待之前I/O調用結束。
事件編程方式:輕量級,松耦合,只會關注事務點。
單線程弱點:
無法利用多核CPU
錯誤會引起整個應用退出,應用的健壯性
大量計算占用CPU導致無法繼續(xù)調用異步I/O。
瀏覽器中JavaScript與UI公用一個線程,JavaScript長時間執(zhí)行會導致UI的渲染和響應被中斷。
在Node中,長時間占用CPU到孩子后續(xù)的異步I/O發(fā)不出調用,已經(jīng)完成的異步I/O的回調函數(shù)也會得不到執(zhí)行。
解決:
child_progress:解決單線程中大量算量的問題
Master-Worker:管理各個工作進程 (管理子進程)
啟用一個完全獨立的進程,將需要計算的程序發(fā)送給進程。通過時間將結果傳遞回來。(消息傳遞的方式來傳遞運行結果)
采用消息傳遞的方式: 保持應用模型的簡單和低依賴。
Node的應用場景
I/O密集型
面向網(wǎng)絡且擅長并行I/O,能夠有效的組織起更多的硬件資源。 利用事件循環(huán)的處理機制,資源占用極少。
不是很擅長CPU密集型業(yè)務,但是可以合理調度
通過編寫C/C++擴展的方式更高效的利用CPU
與遺留系統(tǒng)問題和平共處
LinkeDin, 雪球財經(jīng)
分布式應用
阿里的數(shù)據(jù)平臺,對Node的分布式應用 分布式應用要求:對可伸縮性要求高。 具體應用: 中間層應用NodeFox,ITer,將數(shù)據(jù)庫集群做了劃分和映射,查詢調用一句是針對單張表進行SQL查詢,中間層分解查詢SQL,并行的去多態(tài)數(shù)據(jù)庫中獲取數(shù)據(jù)并合并。 NodeFox作用:實現(xiàn)對多臺MySQL數(shù)據(jù)的查詢 ITer作用:查詢多個數(shù)據(jù)庫(指的是不同數(shù)據(jù)庫,MySQL,Oracle等)Node的使用者
前后端編程語言環(huán)境統(tǒng)一:雅虎開放了Cocktail框架
Node帶來的高性能的I/O用于實時應用:Voxer和騰訊
Voxer:實時語音 騰訊:Node應用在長連接,實時功能
并行I/O使得使用者可以更高效地利用分布式環(huán)境:阿里巴巴和eBay
利用Node并行I/O的能力,更高校的使用已有的數(shù)據(jù)
并行I/O,有效利用穩(wěn)定接口提升Web渲染能力:雪球財經(jīng)和LinkedIn
云計算平臺提供Node支持
游戲開發(fā)領域:網(wǎng)易的pomelo實時框架
工具類應用
模塊機制Node的模塊機制
模塊在引用過程中的編譯,加載規(guī)則
CommonJs規(guī)范為JavaScript提供了一個良好基礎,JavaScript能夠在任何地方運行。
規(guī)范涵蓋:模塊,二進制,Buffer,字符集編碼,I/O流,進程環(huán)境,文件系統(tǒng),套接字,單元測試,Web服務器網(wǎng)管接口,包管理
JavaScript規(guī)范缺陷
沒有模塊系統(tǒng)
標準庫較少
沒有標準接口
缺乏包管理系統(tǒng)
Node借鑒CommonJS的Modules規(guī)范實現(xiàn)了一套非常易用的模塊系統(tǒng)
NPM對Packages規(guī)范的完好支持使得Node在應用開發(fā)過程中更加規(guī)范
CommonJS的模塊規(guī)范
模塊應用
var math = require("math");
在CommonJs規(guī)范中,存在require();方法,這個方法接收模塊標識,以此引入一個模塊的API到當前上下文中。
模塊定義
require(): 引入外部模塊。
exports對象: 導出當前的方法或者變量,而且是唯一導出的出口
module對象:表示自身模塊,而exports是module的屬性。
Node中,一個文件就是一個模塊,將方法掛載到exports對象作為屬性定義導出方式.
模塊標識
傳遞給require() 方法的參數(shù),必須符合小駝峰命名的字符串,或者以 .,..開頭的相對路徑,或者絕對路徑。 js文件可以沒有后綴.js
模塊的意義:
將類聚的方法和變量等限定在私有的作用域中,同時支持引入和導出的功能順暢的連接上下游依賴。
Node中引入模塊的步驟:
1:路徑分析
2:文件定位
3:編譯執(zhí)行
模塊分類
核心模塊:Node提供的模塊
文件模塊:用戶編寫的模塊
核心模塊在Node源代碼的編譯過程中,編譯進了二進制執(zhí)行文件。
Node進程啟動時,部分核心模塊就直接被加載近內存中。
文件模塊是在運行時動態(tài)加載,需要完成的路徑分析,文件定位,編譯執(zhí)行的過程。
模塊加載過程
優(yōu)先從緩存加載
Node對引入過的模塊都是進行緩存,減少二次引入時的開銷。(Node緩存是編譯和執(zhí)行之后的對象)
不論是核心模塊還是文件模塊,require()方法對同模塊的二次加載都采用緩存優(yōu)先的方式。 不同之處,核心模塊的緩存檢查優(yōu)先于文件模塊的緩存檢查
路徑分析和文件定位
○ 模塊標識符分析
核心模塊
路徑形式的文件模塊
自定義模塊
模塊標識符分類:
核心模塊:http,fs,path 等
.,..開始的相對路徑的文件模塊
以/開始的絕對路徑文件模塊
非路徑形式的文件模塊(自定義模塊)
核心模塊
核心模塊的優(yōu)先級僅此于緩存加載,在Node的源碼編譯過程中,已經(jīng)編譯為二進制代碼,其加載過程最快。
如果想加載和核心模塊標識相同的模塊,必須選擇修改標識或換用路徑的方式。
路徑形式的文件模塊
.,..開始的相對路徑的文件模塊。require();方法會將路徑轉為真實路徑,并以真實路徑作為索引,將編譯執(zhí)行后的結果存放入緩存中。
文件模塊指明了確切的文件位置,在查找過程中節(jié)約大量時間,其加載速度慢于核心模塊。
自定義模塊
特殊的文件模塊,可能是一個文件或包的形式。這類模塊查找最費時,也是所有方式最慢的一種。
模塊路徑:Node在定位文件模塊的具體文件時定制的查找策略。表現(xiàn)為一個路徑組成的數(shù)組。
node_modules 會按照類似JavaScript的原型鏈查查找方式,在加載過程中,Node會租個嘗試模塊路徑中的路徑。直到找到文件為止。(文件路徑越深,模塊查找耗時越多)
○ 文件定位
文件擴展名分析(.js,.node,.json次序補足擴展名)
目錄分析和包
文件擴展名分析
require(); 在分析標識符的過程中,標識符不含有文件擴展名的情況,Node會按照 .js,.node,.json 補足擴展名,以此嘗試
嘗試過程:需要調用fs模塊同步阻塞式判斷文件是否存在。
因為Node是單線程,這邊會引起性能問題。
避免方法:
方法1:如果是.node 和 .json文件,標識符加上擴展名
方法2:同步配合緩存,可以大幅度緩解Node中阻塞式調用的缺陷
目錄分析和包
在分析文件擴展名之后,沒有找到對應的文件,但卻得到一個目錄。當作一個包來處理。
Node在當前目錄下查找package.json(CommonJS包規(guī)范定義的包描述文件),通過JSON.parse();解析出包描述對象,從中取出main屬性指定的文件名進行定位。如果文件名缺少擴展名,將會進去擴展名分析的步驟。
如果main屬性指定的文件名錯誤,或者更沒有package.json的文件,Node會將index當作默認文件名,然后一次查找index.js, index.node , index.json
如果在目錄分析的過程中沒有定位成功任何文件,則自定義模塊進入下一個模塊路徑進行查找。如果模塊路徑數(shù)組都遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗的異常。
○ 編譯模塊
.js文件。通過fs模塊同步讀取文件后編譯執(zhí)行
.node文件
用C/C++編寫的擴展文件 通過dlopen()方法加載最后編譯生成的文件
.json文件
通過fs模塊同步讀取文件后, 用JSON.parse()解析返回結果
其它擴展名文件。它們被當作.js文件載入
在Node中,每個文件模塊都是一個對象。
每一個編譯成功的模塊都會將其目錄作為索引緩存在Module._cache對象上.
JavaScript模塊的編譯
在編譯過程中,Node對獲取的JavaScript文件內容進行了頭尾包裝。
(function ( exprots, require, moduel, __filename, __dirname ) { });
包裝之后,對每個文件之間進行了作用域隔離,包裝之后的代碼會通過vm原生模塊的runIntThisContext();方法執(zhí)行 (類似eavl,只是具有明確上下文,不污染全局). 返回一個具體的function 對象。 最后,將當前模塊對象的exports屬性,require()方法,module[模塊對象自身],以及在文件定位中得到的完整文件路徑和文件目錄作為參數(shù)傳遞給這個funciton() 執(zhí)行。
在執(zhí)行之后,模塊中的exports 屬性被返回給了調用方。exports屬性上的任何方法和屬性在外界都可以被調用得到。
有了exports的情況下,為何還存在module.exports.
給exports重新賦值,exports對象是通過形參的方式傳入的,直接賦值會改變形參的引用,但是不能改變作用域外的值
Node的核心模塊在編譯成可執(zhí)行文件的過程中編譯近了二進制文件。
核心模塊:
C/C++編寫,存放在Node項目中src目錄下
JavaScript編寫,存放在lib目錄下
JavaScript核心模塊的編譯過程
轉存為C/C++代碼
編譯為JavaScript核心模塊
轉存為C/C++代碼
Node采用V8的js2c.py工具,將所有內置的JavaScript代碼("src/node.js 和 lib/*.js") 轉換成C++里的數(shù)組,生成node_natives.h頭文件
啟動Node進程時,JavaScript代碼直接加在進內存中。在加載過程中,JavaScript核心模塊經(jīng)歷標識符分析后直接定位到內存中,比普通的文件模塊從磁盤從查找快很多。
編譯為JavaScript核心模塊
JavaScript核心模塊與文件模塊區(qū)別:
獲取源代碼的方式(核心模塊是從內存中加載的)以及緩存執(zhí)行結果的位置。
源文件通過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實現(xiàn)包裝或向外到處。
內建模塊: 純C/C++編寫的部分
JavaScript主外實現(xiàn)封裝的模式是Noe能夠提高性能的常見方式.
Node的buffer,crypto,evals,fs,os等模塊都是部分通過C/C++編寫 (不直接被用戶調用)
內建模塊的組織形式
每一個內建模塊定義之后,都通過NODE_MODULE宏將模塊定義到node命名空間中。
內建模塊的導出
在Node的所有模塊類型中,存在依賴關系。
一般的,不推薦文件模塊直接調用內建模塊。如需調用,直接調用核心模塊。
因為:核心模塊中基本都封裝了內建模塊。
Node在啟動時,會生成一個全局變量process,并提供Binding()方法謝祖加載內建模塊。
轉為C/C++數(shù)組存儲,通過process.binding("natives"); 取出防止NativeModule.source中.
在加載內建模塊時,先創(chuàng)建exports空對象,然后調用get_builtin_module() 方法取出內建模塊對象,通過執(zhí)行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")
編寫核心模塊
前提條件:
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模塊調用擴展模塊。
包與NPM包結構
package.json:包描述文件
bin:用于存放可執(zhí)行二進制文件的目錄
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
從非官方源安裝:npm install underscore --registry=http://registry.url
NPM鉤子命令
package.json中的script字段:讓包在安轉或者卸載過程中提供鉤子機制
"scripts": { "preinstall": "preinstall.js", "install": "install.js", "uninstall": "uninstall.js", "test": "test.js" }
在執(zhí)行npm install
NPM潛在問題
NPM平臺上面包質量良莠不齊
Node代碼可以運行在服務端,需要考慮安全問題
模塊的側重點
Node的模塊引入過程,幾乎全部都是同步。
AMD規(guī)范
AMD規(guī)范:是CommonJS模塊規(guī)范的一個延伸
//通過數(shù)組引入依賴 ,回調函數(shù)通過形參傳入依賴 define(["someModule1", ‘someModule2’], function (someModule1, someModule2) { function foo () { /// someing someModule1.test(); } return {foo: foo} });
CMD規(guī)范
CMD規(guī)范:玉伯提出,區(qū)別定義模塊和依賴引入
//CMD define(function (requie, exports, module) { //依賴 就近書寫 var a = require("./a"); a.test(); //軟依賴 if (status) { var b = requie("./b"); b.test(); } });
1.對于依賴的模塊AMD是提前執(zhí)行,CMD是延遲執(zhí)行。RequireJS從2.0開始,也改成可以延遲執(zhí)行(根據(jù)寫法不同,處理方式不通過)。
2.CMD推崇依賴就近,AMD推崇依賴前置。
UMD規(guī)范
兼容多種模塊規(guī)范(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
事件循環(huán)是異步實現(xiàn)的核心,與瀏覽器中的執(zhí)行模型基本保持一致。
古老的Rhino,是較早服務器上運行JavaScript,但是執(zhí)行模型并不像瀏覽器采用事件驅動.而是使用其它語言一樣采用同步I/O作為主要模型.
為什么要異步I/O用戶體驗,消耗時間為max(M,N)
資源分配,讓單線程遠離阻塞,更好利用CPU
用戶體驗
異步概念: 瀏覽器中JavaScript在單線程上執(zhí)行,還與UI渲染共用一個線程。 表示JavaScript在執(zhí)行的時候UI渲染和響應是處于停止狀態(tài)
資源分配
單線程同步會因為阻塞I/O導致硬件資源的不到更優(yōu)使用。多線程編程中的死鎖,狀態(tài)同步等問題。
選擇: 利用單線程,原理多線程死鎖,狀態(tài)同步等問題,利用異步I/O,讓單線程原理阻塞,更好的使用CPU。
異步I/O實現(xiàn)現(xiàn)狀操作系統(tǒng)內核對于I/O只有兩種方式:阻塞與非阻塞。
在調用阻塞I/O時,應用程序需要等待I/O才會返回結果.
阻塞I/O特點:調用之后一定要等到系統(tǒng)內核層面完成所有操作后,調用才結束。
異步I/O與非阻塞I/O
操作系統(tǒng)對計算機進行了抽象,將所有的輸入輸出設備抽象為文件。內核在進行文件I/O操作時,通過文件描述符進行管理。
應用程序如果需要進行I/O調用,需要先打開文件描述符,然后再更具文件描述符去實現(xiàn)文件的數(shù)據(jù)讀寫。
阻塞I/O: 完成整個獲取數(shù)據(jù)的過程。
非阻塞I/O: 不帶數(shù)據(jù)直接返回,要獲取數(shù)據(jù),還需要通過文件描述符再次讀取。
為了獲取完整數(shù)據(jù),應用程序需要重復調用I/O操作來確認是否完成。這種重復調用判斷操作是否完成的叫做輪詢;
輪詢: CPU決策如何提供周邊設備服務的方式。 又稱為 “程控輸出入” (Programmed I/O)。
輪詢法: 由CPU定時發(fā)出詢問,依次詢問每一個周邊設備是否需要其服務,有即給予服務。服務結束再詢問下一個周邊,重復詢問。
非阻塞I/O缺點:輪詢去確認是否完全完成數(shù)據(jù)獲取,會讓CPU處理狀態(tài)判斷,是對CPU資源的浪費。需要減小I/O狀態(tài)判斷的CPU損耗.
輪詢技術
read
select
poll
epoll
kqueue
read : 重復調用來檢查I/O的狀態(tài)來完成完整數(shù)據(jù)的讀取。
缺點:性能最低
select : 文件描述符上的事件狀態(tài)來進行判斷
缺點:最多可同時檢查1024個文件描述符.
poll : 鏈表的方式避免數(shù)組長度的限制,能避免不需要的檢查
缺點:文件描述符較多,性能低下。
epoll : I/O事件通知機制。
進入輪詢的時候如果沒有檢測到I/O事件,將會進行休眠,知道事件發(fā)生將它喚醒。
事件通知,執(zhí)行回調的方式。而不是遍歷查詢。
特點:不會浪費CPU,執(zhí)行效率較高。
kqueue : 實現(xiàn)方式和epoll類似,存在FreeBSD系統(tǒng)下。
輪詢缺點:
輪詢對于應用程序,仍然是一種同步,應用程序讓然需要等待I/O完全返回,依舊花費很多時間來等待。等待期間CPU要么用于遍歷文件描述符的狀態(tài),要么用戶休眠等待事件發(fā)生。
理想的非阻塞異步I/O
應用程序發(fā)起非阻塞調用,無需通過遍歷或者事件喚醒等方式輪詢,可以直接處理下一個任務。
在I/O完成后通過信號或回調將數(shù)據(jù)傳遞給應用程序。
現(xiàn)實中的異步I/O
*nix平臺下采用libeio配合libev實現(xiàn)I/O部分
windows平臺采用IOCP是實現(xiàn)異步I/O
部分線程阻塞I/O 或者 非阻塞I/O + 輪詢技術 -> 完成數(shù)據(jù)獲取。
一個線程計算處理
通過線程之間的通信將I/O得到的數(shù)據(jù)進行傳遞。
IOCP: 調用異步方法,等待I/O完成之后的通知,執(zhí)行回調,用戶無序考慮輪詢。(實現(xiàn)原理:線程池原理)
*nix將計算機抽象:磁盤文件,硬件,套接字,等幾乎所有計算機資源抽象為文件。
在Node中,無論是*nix還是Windows平臺,內部完成I/O任務的另有線程池。
Node的異步I/O事件循環(huán)
觀察者
請求對象
執(zhí)行回調
事件循環(huán),觀察者,請求對象,I/O線程池共同構成Node異步I/O模型的基本要素。
事件循環(huán)
Node 自身的執(zhí)行模型 -- 事件循環(huán)。
在進程啟動時,Node會穿件一個類似于while(true)的循環(huán),每執(zhí)行一次循環(huán)體的過程 稱之為: Tick(標記,打勾) .每個Tick的過程就是查看是否有事件待處理,如果有,就取出事件及其相關的回調函數(shù)。如果存在關聯(lián)的回調,就執(zhí)行它們。然后進入下個循環(huán),如果不再有事件處理,就退出進程。
觀察者
每個事件循環(huán)中有一個或者多個觀察者,而判斷是否有事件要處理的過程就是想這些觀察者詢問是否有要處理的事件。
瀏覽器類似事件觀察機制: 時間可能來自用戶的點擊或者加載某些文件時產(chǎn)生,而這些產(chǎn)生的事件都有對應的觀察者。
事件循環(huán)是一個典型的生產(chǎn)者/消費者模型,$watch $digest 機制。 異步I/O,網(wǎng)絡請求則是事件的生產(chǎn)者,遠遠不斷為Node提供不同類型的事件,這些事件被傳遞到對應的觀察者那里,事件循環(huán)則從觀察則那里取出事件并處理。
請求對象
請求對象: 從JavaScript 發(fā)起調用到內核執(zhí)行完I/O操作的過度過程中,存在一種中間產(chǎn)物.
請求對象是異步I/O過程中的重要中間產(chǎn)物,所有的狀態(tài)都保存在這對象中,包括送入線程池等待執(zhí)行以及I/O操作完畢后的回調處理。
Node中的異步I/O調用,回調函數(shù)不由開發(fā)者來調用。
從發(fā)出調用后,到回調函數(shù)被執(zhí)行,中間發(fā)生了什么?
fs.open() 示例:
fs.open = function ( path, flags, mode, cb ) { binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, cb); };
Node經(jīng)典調用方式:從JavaScript調用Node的核心模塊,核心模塊調用C++內建模塊,內建模塊通過libuv進行系統(tǒng)調用。
調用的uv_fs_open() -> FsReqWrap (請求對象,作用:JavaScript層傳入的參數(shù)和當前方法都封裝對象中)
回調函數(shù)被設置在FsReqWrap.oncomlete_sym 屬性上
req_wrap -> object_ -> Set(oncomlete_sym, callback);
包裝完之后,Windows調用:QueueUserWorkItem() 將FsReqWrap對象推入線程池中等待執(zhí)行
QueueUserWrokITem(&uv_fs_thread_proc, req, WT_EXTCUTEDEFAULT);
QueueUserWorkItem()
參數(shù)1:執(zhí)行的方法引用 fs_open() 的引用 uv_fs_thread_proc
參數(shù)2:uv_fs_thread_proc() 方法執(zhí)行時所需參數(shù)。
參數(shù)3:執(zhí)行的標志。
調用完成之后,JavaScript調用立即返回,由JavaScript層面發(fā)起的異步調用的第一階段結束。
JavaScript線程可以繼續(xù)執(zhí)行當前任務的后續(xù)操作。當前的I/O操作在線程池中等待執(zhí)行,不管是否阻塞I/O,都不會影響到JavaScript線程的后續(xù)執(zhí)行。
執(zhí)行回調
回調通知,完成完整異步I/O的第二部分。
線程池中的I/O操作調用完畢后,會將結果存儲在req -> reslut 屬性上。然后調用 PostQueuedCompletionStatus(); 通知IOCP,告知當前對象操作已經(jīng)完成:
PostQueuedCompletionStatus( (loop)->iocp, o, o, &(req)->overlappped )
PostQueuedCompletionStatus()作用: 向IOCP提交執(zhí)行狀態(tài),并將線程歸還給線程池。提交狀態(tài)
GetQueuedCompletionStatus() 作用: 提取
每個Tick的執(zhí)行中,會調用IOCP相關的GetQueuedCompletionStatus()方法檢查線程池中是否有執(zhí)行完的請求,如果存在,會將請求加入到I/O觀察者的隊列中,然后將其當作事件處理。
I/O觀察者回調函數(shù)的行為: 取出請求對象的result屬性作為參數(shù),取出oncomlplete_sym屬性作為方法,然后調用執(zhí)行。以此達到調用JavaScript中傳入的回調函數(shù)的目的。
非I/O的異步API定時器
定時器: setTimeout(),setInterval()
調用setTimeout()或者setInterval()創(chuàng)建的定時器會被插入到定時器觀察者內部的一個紅黑樹(作用:實現(xiàn)關聯(lián)數(shù)組)中。
每次Tick執(zhí)行時,會從該紅黑樹中迭代取出定時器對象,檢查是否超過定時時間,如果超過,就形成一個事件,它的回調函數(shù)將立即執(zhí)行。
定時器問題:并非精確的(在誤差范圍內)。如果某一次循環(huán)占用的時間較多。那么下次循環(huán)時,也許超時很久。
例如:setTImeout()設定一個任務在10毫秒后執(zhí)行,但是9毫秒后,有一個任務占用了5毫秒的CPU時間片,再次輪到定時器時,時間就已經(jīng)過期4毫秒。
process.nextTick()
process.nextTick(): 操作比較輕量,高效
會將回調函數(shù)放入隊列中,在下一輪Tick時取出執(zhí)行,每輪循環(huán)中會將數(shù)組中的回調函數(shù)全部執(zhí)行完。
setImmediate()
setImmediate()與上者類似,但優(yōu)先級低于process.nextTick()
原因:事件循環(huán)對觀察者的檢查是有先后順序。 process.nextTick(); 屬于idle觀察者,setImmediate() 屬于check觀察者。
在每一輪循環(huán)檢查中,idle觀察者I/O觀察者,I/O觀察者先于check觀察者
會將結果保存在鏈表中。每輪循環(huán)中執(zhí)行鏈表中的一個回調函數(shù)。
process.nextTick(function () { console.log("nextTick延遲執(zhí)行"); }); setImmediate(function () { console.log("setImmediate延遲執(zhí)行"); }); console.log("正常執(zhí)行"); // 執(zhí)行結果 // 正常執(zhí)行 // nextTick延遲執(zhí)行 // setImmediate延遲執(zhí)行事件驅動和高性能服務器
事件驅動的本質:通過主循環(huán)加事件觸發(fā)的方式來運行程序。
經(jīng)典模型
同步式
一次只能處理一個請求,并且其余請求都處于等待狀態(tài)。
每進程/每請求
每個請求啟動一個進程,這樣可以處理多個請求。 缺點:不具備擴展性。(因為系統(tǒng)資源有限)
每線程/每請求 (Apache)
為每個請求啟動一個線程來處理。 優(yōu)點:線程比進程要輕量 缺點:每個線程都占用一定內存,當大并發(fā)請求到來時,內存會很快耗光,導致服務器緩慢。
線程(程序執(zhí)行流的最小單元)
進程(一段程序的執(zhí)行過程)
一個進程中包括多個線程
高階函數(shù)
高階函數(shù):把函數(shù)作為輸入或返回。作為參數(shù)或者返回值的函數(shù)
偏函數(shù)用法:創(chuàng)建一個調用另外一部分——參數(shù)或變量已經(jīng)預置的函數(shù)——的函數(shù)的用法。
通過指定部分參數(shù)來創(chuàng)建一個新的函數(shù)的形式。
優(yōu)勢
優(yōu)勢:基于事件驅動的非阻塞I/O模型
效果:非阻塞I/O可以是CPU與I/O并不相互依賴等待,讓資源得到更好的利用。
難點
異常處理
異步I/O的實現(xiàn)主要包含兩個階段:提交請求和處理結果。 這兩個階段中間有事件循環(huán)的調度,兩者彼此不關聯(lián)。 異步方法則通常在第一階段提交請求后立即返回,因為異常并不發(fā)生在這個階段。
函數(shù)嵌套過深
阻塞代碼
多線程編程
異步轉同步
異步編程解決方案事件發(fā)布/訂閱模式
事件監(jiān)聽器模式:回調函數(shù)的事件化,又稱發(fā)布/訂閱模式 (鉤子(hook)機制)
Node中的很多對象大多具有黑盒的特點,功能點較少。
事件發(fā)布/訂閱模式自身并無同步和異步調用的問題。但在Node中,emit()調用多半是伴隨著時間循環(huán)而異步觸發(fā)。
如果對一個時間添加了超過10個偵聽器,將會得到一條警告。(原因:偵聽器太多可能導致內存泄漏)。可以通過 emitter.setMaxListeners(0); 去掉這個限制。
由于事件發(fā)布會引起一些列偵聽器執(zhí)行,如果事件相關的偵聽器過多,可能存在過多占用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);
利用事件隊列解決雪崩問題
事件訂閱/發(fā)布模式中,通常由一個once()方法。
作用:偵聽器只能執(zhí)行一次,在執(zhí)行之后就會將它與事件的關聯(lián)解除。
解決:過濾一些重復性的事件響應。
問題:緩存中存放在內存中,訪問速度十分快,用于加速數(shù)據(jù)訪問,讓絕大所數(shù)的請求不必重復去做一些抵消的數(shù)據(jù)讀取。
雪崩問題:高訪問量,大并發(fā)量的情況下緩存失效的情景,此時大量的請求同時涌入數(shù)據(jù)庫中,數(shù)據(jù)庫無法同時承受如此大的查詢請求。
解決方案:添加一個狀態(tài)鎖。
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() 方法,將所有請求的回調都壓入事件隊列中。保證回只會被執(zhí)行一次。
Gearman異步應用框架中,利用noce()方法產(chǎn)生的效果。
多異步之間的協(xié)作方案
事件與偵聽器的關系是一對多,在異步編程中,會出現(xiàn)事件與偵聽器的關系多對一的情況。
一個業(yè)務邏輯可能依賴兩個通過回調或事件傳遞的結果。
問題:多個異步場景中的回調函數(shù)的執(zhí)行順序,且回調之間沒有任何交集。(多對一)
解決辦法:通過第三方函數(shù)和第三方變量來處理異步協(xié)作 -- 哨兵變量。
var after = function ( times, cb ) { var count = 0, resluts = {}; return function ( key, value) { resluts[key] = value; count++; if ( coutn === times ) { cb(resluts); } } }
哨兵變量和發(fā)布/訂閱模式完成多對多方案
// 哨兵變量 var after = function ( times, cb ) { var count = 0, resluts = {}; return function ( key, value) { resluts[key] = value; count++; if ( coutn === times ) { cb(resluts); } } } // 哨兵變量和發(fā)布/訂閱模式完成多對多方案 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();來訂閱多個事件,當每個時間都被觸發(fā)后,偵聽器才會執(zhí)行。
tail(); 偵聽器在滿足條件之后只會執(zhí)行一次。
場景:從一個接口多次讀取數(shù)據(jù),此時觸發(fā)的事件名或許是相同的。
利用after();方法實現(xiàn)時間在執(zhí)行多少次后執(zhí)行偵聽器的單一事件組合訂閱方式。
var proxy = new EventProxy(); proxy.after("data", 10, function ( datas ) { // TODO });
EventProxy原理
每個非all事件出發(fā)時都會觸發(fā)一次all事件。
EventProxy是將all當作一個事件流的攔截層,在其中注入一些業(yè)務來處理單一時間無法解決的異步問題。
EventProxy的異常處理
exports.getCountent = function ( cb ) { var ep = new EventProxy(); ep.all("tpl", "data", function ( tpl ,data ) { // 成功回調 cb(null, { template: tpl, data: data }); }); // 綁定錯誤處理函數(shù) ep.fail(cb); fs.readFile("template.tpl", "utf-8", ep.done("tpl")); db.get("some sql", ep.done("data")); } // EventProxy模塊提供fail()和done();實例方法來優(yōu)化異常處理。
Promise/Deferred模式
使用發(fā)布訂閱模式缺點:使用事件的方式,執(zhí)行流程需要被預先設定。
場景:先執(zhí)行異步調用,延遲傳遞處理的方式。
Promises/A
Promises/A:對單個異步操作抽象定義
Promise 操作只會處在3中狀態(tài):未完成態(tài),完成態(tài)和失敗態(tài)。
Promise 的狀態(tài)只會出現(xiàn)從未完成態(tài)向完成態(tài)或失敗態(tài)轉化,不能逆反。完成態(tài)和失敗態(tài)不能互相轉化。
Promise的狀態(tài)一旦轉化,將不能被更改。
Promises/A要求Promise對象只需具備then()方法即可。
接受完成態(tài),錯誤態(tài)的回調方法,在操作完成或出現(xiàn)錯誤時,將會調用對應方法。
可選擇支持progress事件回調作為第三個方法。
then()方法之接受function對象,其余對象將被忽略。
then()方法繼續(xù)返回Promise對象,實現(xiàn)鏈式調用。
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 ) { // 完成態(tài) if ( typeof fulfilledHandler === "function" ) { this.once("success", fulfilledHandler); } // 失敗態(tài) if ( typeof errorHandler === "function" ) { this.once("error", errorHandler); } // 執(zhí)行 if ( typeof progressHandler === "function" ) { this.on("progress", progressHandler); } return this; }
Deferred 延遲對象:觸發(fā) Promise對象中滿足條件的函數(shù),并實現(xiàn)這些功能。
// Deferred var Deferred = function () { this.state = "unfulfilled"; this.promise = new Promise(); } // 實現(xiàn)完成態(tài) Deferred.prototype.resolve = function ( obj ) { this.state = "fulfilled"; this.promise.emit("success", obj); } // 實現(xiàn)失敗態(tài) Deferred.prototype.reject = function ( err ) { this.state = "failed"; this.promise.emit("error", err); } Deferred.prototype.progress = function ( data ) { this.promise.emit("progress", data); }
Deferred主要用于內部,用于維護異步模型的狀態(tài)
Promise作用于外部,通過then()方法暴露給外部以添加自定義邏輯。
Promise/Deferred -- 響應對象
高級接口:不容易變化,不再有低級接口的靈活性。
低級接口:可以構成更多更負責的場景。
Q模塊是Promises/A規(guī)范的一個實現(xiàn)
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提議的實現(xiàn)。
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中多異步協(xié)議
Promise解決的是:單個異步操作中存在的問題.
所有操作成功,這個異步操作才成功,一旦其中一個異步操作失敗,整個一部操作就失敗。
Promise的進階知識
Promise缺點:需要為不同的場景封裝不同的API,沒有直接的原生事件那么靈活。
Promise最主要的是處理隊列操作。
支持序列執(zhí)行的Promise
promise() .then(obj.api1) .then(obj.api2) .then(obj.api3) .then(function ( valu4 ) { // Do something with value4 }, function ( error ) { }) .done();
Promise支持鏈式執(zhí)行步驟:
1:將所有的回調都存到隊列中
2:Promise完成時,租個執(zhí)行回調,一旦檢測返回了新的Promise對象,停止執(zhí)行,然后將當前Deferred對象的promise引用改變?yōu)樾碌腜romise對象,并將隊列中的余下的回調轉交給它。
流程控制庫
尾觸發(fā)與Next
尾觸發(fā):手動調用才能持續(xù)執(zhí)行后續(xù)調用,關鍵字next
應用:Connect中間件中.Connect中間件傳遞請求對象,響應對象和尾觸發(fā)函數(shù),通過隊列形成一個處理流。
// Connect的核心實現(xiàn) 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中間件這種尾觸發(fā)模式并不是要求每個中間方法都是異步,但如果每步驟都采用異步來完成,實際上是串行化的處理。
流式處理將一些串行的邏輯扁平化。串行化:將對象存儲到介質(如文件,內存緩沖區(qū)等)中或是以二進制方式通過網(wǎng)絡傳輸。然后需要使用的時候通過:反串行化從這些連續(xù)的字節(jié)數(shù)據(jù)重新構建一個與原始對象狀態(tài)相同的對象。
作用:有時候,需要將對象的狀態(tài)保存下來,在需要時再會將對象恢復。
對象通過寫出描述自己的狀態(tài)數(shù)值來記錄自己,這個過程叫對象串行化。async
流程控制模塊async
○異步的串行執(zhí)行
series(); 實現(xiàn)一組任務的串行執(zhí)行
var fs = require("fs"); var async = require("async"); // 異步串行執(zhí)行 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(); 方法中傳入的函數(shù)cb();并非有使用者指定。此處的回調函數(shù)有async通過高階函數(shù)的方式注入。每個cb();執(zhí)行時會將結果保存起來,然后執(zhí)行下一個調用,知道結束所有調用。最終的回調函數(shù)執(zhí)行時,隊列里的異步調用保存的結果以數(shù)組的方式傳入。 // 異常處理規(guī)則:一旦出現(xiàn)異常,就結束所有調用,并將異常傳遞給最終回調函數(shù)的第一個參數(shù)。○異步的并行執(zhí)行
并行作用:提升性能parallel(); 以并行執(zhí)行一些異步操作。
var fs = require("fs"); var async = require("async"); // 異步的并行執(zhí)行 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 ); }); // 通過注入的回調函數(shù)。 // parallel(); 對于異常的判斷依然是一旦某個異步調用產(chǎn)生了異常,就會將異常作為第一個參數(shù)傳遞給最終的回調函數(shù)。○異步調用的依賴處理
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(); 根據(jù)依賴自動分析,以最佳的順序執(zhí)行業(yè)務。
實現(xiàn)復雜的依賴關系,業(yè)務中或是異步,或是同步。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接受任意數(shù)量的任務,所有任務都將會串行依次執(zhí)行。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()方法,將異步調用的結果傳遞給下一個任務作為參數(shù),并調用執(zhí)行○并行任務執(zhí)行
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 ); }); // 異常處理:一旦有一個異常產(chǎn)生,這個異常會作為下一個方法的第一個參數(shù)傳入。○結果分組
異步并發(fā)控制
this.group();
將返回的數(shù)據(jù)保存在數(shù)組中原因:并發(fā)量過大,下層服務器將會吃不消。如果對文件系統(tǒng)進行大量并發(fā)調用,操作系統(tǒng)的文件描述符數(shù)量將會被瞬間用光。
bagpipe的解決方案
通過一個隊列來控制并發(fā)量
如果當前活躍(指調用發(fā)起但為執(zhí)行回調)的異步調用量小于限定值,從隊列中取出執(zhí)行。
如果活躍調用達到限定值,調用暫時存放在隊列中。
每個異步調用結束時,從隊列中取出心兒異步調用執(zhí)行。
bagpipe中的push();和full事件
var Bagpipe = require("bagpipe");// 設定最大并發(fā)數(shù)為10 var bagpipe = new Bagpipe(10); for ( var i=0; i<100; i++ ) { bagpipe.push(async, function () { // 異步回調執(zhí)行 }); } bagpipe.on("full", function ( length ) { console.warn("底層系統(tǒng)處理不能及時完成,隊列擁堵,目前隊列長度為:"+ length); });拒絕模式
作用:大量異步調用需要分場景,需要實時方面就快速返回。
在設定并發(fā)數(shù)時,參數(shù)設置為true// 設定最大并發(fā)數(shù)為10 var bagpipe = new Bagpipe(10, { refuse: true });超時控制
原因:異步調用耗時太久,調用產(chǎn)生的速度遠遠高于執(zhí)行的速度。防止某些異步調用使用太多的時間。
實現(xiàn):將執(zhí)行時間太久的異步調用清理出活躍隊列。通過,設定時間閥值。
效果:排隊中的異步調用越快執(zhí)行。var bagpipe = new Bagpipe(10, { timeout: 3000 });async的解決方案
parallelLimit(); 處理異步調用的限制
缺點:無法動態(tài)增加并行任務。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 ) { // 用于限制并發(fā)數(shù)量的參數(shù),任務只能同時并發(fā)一定數(shù)量,而不是無限制并發(fā) console.log( resluts ); });queue(); 處理異步調用限制,能夠的動態(tài)增加任務。
一般用于:遍歷文件目錄等操作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位系統(tǒng)下約1.4GB,32位系統(tǒng)下約為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 已經(jīng)申請到堆內存, heapUsed 當前使用量 // 單位:bytes當代碼中聲明變量并賦值時,所使用對象的內存就分配在堆中。如果已經(jīng)申請的堆空想內存不夠分配新的對象,將繼續(xù)申請堆內存,直到堆的代銷超過V8的限制為止。
V8限制堆大小原因:
V8最初為瀏覽器而設計,不太可能使用到大量內存的場景。
V8垃圾回收機制的限制。
1.5GB的垃圾回收堆內存為例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引起JavaScript線程暫停執(zhí)行的時間,在這樣的時間花銷下,應用的性能和響應能力都會直線下降。考慮直接限制堆內存。Node可以在啟動調節(jié)內存限制大小
node --max-old-spce-size=1700 test.js // 單位MB node --max-new-spce-size=1024 test.js // 單位 KB // 一旦生效就不能再動態(tài)改變。V8的垃圾回收機制
1:V8主要的垃圾回收算法
V8垃圾回收策略主要基于分代式垃圾回收機制。
實際應用中,對象的生存周期長短不一,不同的算法只能針對特定情況具有最好的效果。V8的內存分代
在V8中,主要講內存分為新生代和老生代。
新生代:對象為存活時間較短對象。 --max-new-space-size命令行參數(shù)設置 新生代內存空間大小(最大值:64位操作系統(tǒng)32MB,32位操作系統(tǒng)16MB)
老生代:對象為存活較長或常駐內存的對象。--max-old-spce-size命令行參數(shù)設置 老生代內存空間大小。(64位操作系統(tǒng)下:1400MB,32位系統(tǒng)700MB)使用缺陷:在啟動Node時就指定,無法根據(jù)V8內存使用情況自動擴充。
默認情況下,V8堆內存最大值:64位操作系統(tǒng):1464MB,32位操作系統(tǒng)732MB。(4 * reserved_semispce_size + max_old_generation_size_)
○ Scavenge算法
新生代中的對象主要通過Scavenge算法進行垃圾回收。
Scavenge采用了:Cheney算法。Cheney算法:復制的方式實現(xiàn)的垃圾回收算法。
將堆內存一分為二,每一部分空間成為semispce。這兩個semispce空間中,只有一個處于試用,另一個處于閑置狀態(tài)。處于使用狀態(tài)的semispce空間稱成為From空間。處于限制狀態(tài)的空間成為To空間。分配原則:分配對象時,顯示在Form空間中進行分配,當開始進行垃圾回收時,會先檢查From空間中的存活對象,這些存活對象將被復制到To空間中,而非存活對象占用的空間將被釋放。完成賦值后,F(xiàn)orm空間和To空間的角色發(fā)生對換。
垃圾回收的過程中,就是通過將存活對象在兩個semispce空間之間進行復制。
Scavenge缺點:只能使用堆內存中的一半內存。
優(yōu)點:時間效率高 (犧牲空間換取時間)
合適于新生代對象中,因為:新生代中的對象的生命周期比較短。當一個對象進過多次賦值依然存活時,它將會被認為是生命周期較長的對象。會被轉移到老生代中。采用新的算法進行管理。
From空間中的存活對象在復制到TO空間之前需要進行檢查。在一定條件下,需要將存活周期長的對象移動到老生代中,也就是完成對象晉升。
晉升條件:
經(jīng)歷一次Scavenge回收
在默認情況下,V8的對象分配主要集中在From空間中,對象從From空間中復制到To空間時,會檢查它的內存地址來判斷這個對象是否已經(jīng)經(jīng)歷過一次Scavenge回收。如果經(jīng)歷過了,就將該對象從From空間復制到老生代空間中,如果沒有,則復制到TO空間中。
To空間的內存占用比。
當要從From空間復制一個對象到To空間時,如果To空間已經(jīng)使用超過25%,則這個對象直接晉升到老生代空間中。(25%限制值的原因,當這個次Scavenge回收完成后,To空間將會變成From空間,接下來的內存分配將在這個空間中進行。)○ Mark-Sweep & Mark-Compact
存活對象占用較大比重,采用Scavenge方式產(chǎn)生問題:
1:存活對象較多,復制存活對象的效率將會很低
2: 浪費一般空間的問題。Mark-Sweep標記清除:標記和清除,兩個階段。
Mark-Sweep在標記階段遍歷堆中所有對象,并標記著活著的對象。在隨后的清除階段只清除沒有被標記的對象。
產(chǎn)生問題:進行一次標記清除回收后,內存會出現(xiàn)不連續(xù)的狀態(tài)。
原因:內存碎片會對后續(xù)的內存分配造成問題,很可能出現(xiàn)需要分配一個大對象的情況,所有的碎片空間無法完成此次分配。就會提前出發(fā)垃圾回收。Mark-Compact 標記整理
解決:Mark-Sweep內存碎片問題。
對象標記死亡后,在整理的過程中,將或者對象往一端移動,移動完成后,直接清理邊界的內存。
缺點:速度慢。V8主要使用Mark-Sweep,在空間不足以對從新生代中晉升過來的對象進行分配時才使用Mark-Compact
○ Incremental Marking (增量標記)
全停頓:將應用邏輯展亭下來,待執(zhí)行完來回收后再回復執(zhí)行應用邏輯。為了避免出現(xiàn)JavaScript應用邏輯與垃圾回收器看到的不一致的情況。三種垃圾回收機制都采用全停頓。
解決辦法:增量標記
垃圾回收與應用邏輯交替執(zhí)行直到標記階段完成。
除了增量標記,V8還引入延遲清理,增量式清理。
讓清理與整理動過也變成增量式。V8對內存限制的設置對于Chrome瀏覽器這種每個選項卡頁面使用一個V8實例而言,內存的使用時綽綽有余。
Node編寫服務端來說,V8垃圾回收特點和JavaScript在單線程上的執(zhí)行情況,垃圾回收時影響性能的因素之一。
高新能的執(zhí)行效率,需要主要讓垃圾回收盡量少的進行,尤其是全堆垃圾回收。○ 查看垃圾回收日志
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中能形成作用域的有函數(shù),width以及全局作用域。
var foo = function () { var local = {}; }內存回收過程
foo(); 函數(shù)在每次被調用時會創(chuàng)建對應的作用域,函數(shù)執(zhí)行結束后,該作用域將會銷毀。
同時作用域中聲明的局部變量分配在該作用域上,隨著作用域的銷毀而銷毀。只被局部變量引用的對象存活周期較短。
由于對象非常小,將會分配在新生代的From空間中,在作用域釋放后,局部變量的elocal失效,其引用的對象將會在下次垃圾回收時被釋放。
標識符查找
與作用域相關的即是標識符查找。作用域鏈
變量的主動釋放。
如果變量是全局變量,(不通過var聲明或定義在global變量上),由于全局作用域需要直到進程退出才能釋放,導致引用的對象常駐內存(常駐在老生代中)。如果需要釋放常駐內存的對象,可以通過delete操作符刪除引用關系。或者將變量重新賦值,讓舊的對象脫離引用關系。 在非全局作用域中,想主動釋放變量引用的對象,也可以通過delete和重新賦值。但是V8中通過delete刪除對象的屬性有可能干擾V8的優(yōu)化,通過賦值方式解除引用更好。閉包
作用域鏈上的對象訪問只能向上,外部無法向內部訪問。
閉包:實現(xiàn)外部作用域訪問內部作用域的中的變量的方法。
高階函數(shù)特性:函數(shù)可以作為參數(shù)或者返回值。閉包問題:
一旦有變量應用這個中間函數(shù),這個中間函數(shù)將不會釋放,同時也會使原始的作用域不會得到釋放,作用域中產(chǎn)生的內存占用也不會得到釋放。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/80025.html
摘要:先天就缺乏一項功能模塊通過標簽引入代碼的方式顯得雜亂無章,語言自身毫無組織和約束能力。與文件模塊區(qū)別地方在于它從內存中加載緩存執(zhí)行結果的位置核心模塊在對象上,文件模塊在對象上未完待續(xù) javascript先天就缺乏一項功能:模塊 javasciprt 通過script標簽引入代碼的方式顯得雜亂無章,語言自身毫無組織和約束能力。人們不得不用命名空間等方式人為地約束代碼,以求達到安全和易用的...
摘要:環(huán)境變量法通過上一節(jié)的源碼分析,我們知道了的作用,那么如何使用或者優(yōu)雅的使用來解決依賴加載問題呢嘗試一最為直接的是,修改系統(tǒng)的環(huán)境變量。 模塊加載痛點 大家也或多或少的了解node模塊的加載機制,最為粗淺的表述就是依次從當前目錄向上級查詢node_modules目錄,若發(fā)現(xiàn)依賴則加載。但是隨著應用規(guī)模的加大,目錄層級越來越深,若是在某個模塊中想要通過require方式以依賴名稱或相對路...
摘要:內部架構上圖表示一個實例的組成部分部分緩沖數(shù)組內部函數(shù)部分緩沖鏈表內部函數(shù)實例必須實現(xiàn)的內部函數(shù)以及系統(tǒng)提供的回調函數(shù)。有三個參數(shù),第一個為待處理的數(shù)據(jù),第二個為編碼,第三個為回調函數(shù)。 Transform流特性 在開發(fā)中直接接觸Transform流的情況不是很多,往往是使用相對成熟的模塊或者封裝的API來完成流的處理,最為特殊的莫過于through2模塊和gulp流操作。那么,Tra...
摘要:進程間通信的目的是為了讓不同的進程能夠互相訪問資源,并進程協(xié)調工作。這個過程的示意圖如下端口共同監(jiān)聽集群穩(wěn)定之路進程事件自動重啟負載均衡狀態(tài)共享模塊工作原理事件二測試單元測試性能測試三產(chǎn)品化項目工程化部署流程性能日志監(jiān)控報警穩(wěn)定性異構共存 內容 9.玩轉進程10.測試11.產(chǎn)品化 一、玩轉進程 node的單線程只不過是js層面的單線程,是基于V8引擎的單線程,因為,V8的緣故,前后...
摘要:數(shù)據(jù)驅動一個核心思想是數(shù)據(jù)驅動。發(fā)生了什么從入口代碼開始分析,我們先來分析背后發(fā)生了哪些事情。函數(shù)最后判斷為根節(jié)點的時候設置為,表示這個實例已經(jīng)掛載了,同時執(zhí)行鉤子函數(shù)。這里注意表示實例的父虛擬,所以它為則表示當前是根的實例。 數(shù)據(jù)驅動 Vue.js 一個核心思想是數(shù)據(jù)驅動。所謂數(shù)據(jù)驅動,是指視圖是由數(shù)據(jù)驅動生成的,我們對視圖的修改,不會直接操作 DOM,而是通過修改數(shù)據(jù)。它相比我們傳...
閱讀 974·2021-11-24 10:42
閱讀 3522·2021-11-19 11:34
閱讀 2659·2021-09-29 09:35
閱讀 2543·2021-09-09 09:33
閱讀 688·2021-07-26 23:38
閱讀 2532·2019-08-30 10:48
閱讀 1399·2019-08-28 18:07
閱讀 433·2019-08-26 13:44