JavaScript絕對是最火的編程語言之一,一直具備很大的用戶羣,隨着在服務端的使用(NodeJs),更是爆發了極強的生命力。編程語言分爲編譯型語言和解釋型語言兩類,編譯型語言在執行以前要先進行徹底編譯,而解釋型語言一邊編譯一邊執行,很明顯解釋型語言的執行速度是慢於編譯型語言的,而JavaScript就是一種解釋型腳本語言,支持動態類型、弱類型、基於原型的語言,內置支持類型。鑑於JavaScript都是在前端執行,並且須要及時響應用戶,這就要求JavaScript能夠快速的解析及執行。css
隨着Web相關技術的發展,JavaScript所要承擔的工做也愈來愈多,早就超越了「表單驗證」的範疇,這就更須要快速的解析和執行JavaScript腳本。V8引擎就是爲解決這一問題而生,在node中也是採用該引擎來解析JavaScript。前端
V8是如何使得JavaScript性能有大幅提高的呢?經過對一些書籍和文章的學習,梳理了V8的相關內容,本文將帶你認識 V8。(該文在 17 年初發佈於公司內網,反響不錯,近來閒暇再次整理做爲知乎的第一篇分享,但願幫助更多的人瞭解 V8 引擎。轉載需通過本人贊成)java
瀏覽器自從上世紀80年代後期90年代初期誕生以來,已經獲得了長足的發展,其功能也愈來愈豐富,包括網絡、資源管理、網頁瀏覽、多頁面管理、插件和擴展、書籤管理、歷史記錄管理、設置管理、下載管理、帳戶和同步、安全機制、隱私管理、外觀主題、開發者工具等。在這些功能中,爲用戶提供網頁瀏覽服務無疑是最重要的功能,下面將對相關內容進行介紹。node
渲染引擎:可以將HTML/CSS/JavaScript文本及相應的資源文件轉換成圖像結果。渲染引擎的主要做用是將資源文件轉化爲用戶可見的結果。在瀏覽器的發展過程當中,不一樣的廠商開發了不一樣的渲染引擎,如Tridend(IE)、Gecko(FF)、WebKit(Safari,Chrome,Andriod瀏覽器)等。WebKit是由蘋果2005年發起的一個開源項目,引發了衆多公司的重視,幾年間被不少公司所採用,在移動端更佔據了壟斷地位。更有甚者,開發出了基於WebKit的支持HTML5的web操做系統(如:Chrome OS、Web OS)。linux
下面是WebKit的大體結構:android
上圖中實線框內模塊是全部移植的共有部分,虛線框內不一樣的廠商能夠本身實現。下面進行介紹:c++
上面介紹了渲染引擎的各個模塊,那麼一張網頁,要經歷怎樣的過程,才能抵達用戶面前?web
首先是網頁內容,輸入到HTML解析器,HTML解析器解析,而後構建DOM樹,在這期間若是遇到JavaScript代碼則交給JavaScript引擎處理;若是來自CSS解析器的樣式信息,構建一個內部繪圖模型。該模型由佈局模塊計算模型內部各個元素的位置和大小信息,最後由繪圖模塊完成從該模型到圖像的繪製。在網頁渲染的過程當中,大體可分爲下面3個階段。正則表達式
上述是一個完整的渲染過程,現代網頁不少都是動態的,隨着網頁與用戶的交互,瀏覽器須要不斷的重複渲染過程。算法
JavaScript本質上是一種解釋型語言,與編譯型語言不一樣的是它須要一遍執行一邊解析,而編譯型語言在執行時已經完成編譯,可直接執行,有更快的執行速度(如上圖所示)。JavaScript代碼是在瀏覽器端解析和執行的,若是須要時間太長,會影響用戶體驗。那麼提升JavaScript的解析速度就是當務之急。JavaScript引擎和渲染引擎的關係以下圖所示:
JavaScript語言是解釋型語言,爲了提升性能,引入了Java虛擬機和C++編譯器中的衆多技術。如今JavaScript引擎的執行過程大體是:
源代碼-→抽象語法樹-→字節碼-→JIT-→本地代碼(V8引擎沒有中間字節碼)。一段代碼的抽象語法樹示例以下:
function demo(name) { console.log(name); }
抽象語法樹以下:
V8更加直接的將抽象語法樹經過JIT技術轉換成本地代碼,放棄了在字節碼階段能夠進行的一些性能優化,但保證了執行速度。在V8生成本地代碼後,也會經過Profiler採集一些信息,來優化本地代碼。雖然,少了生成字節碼這一階段的性能優化,但極大減小了轉換時間。
可是在2017年4月底,v8 的 5.9 版本發佈了,新增了一個 Ignition 字節碼解釋器,將默認啓動,今後以後將與JSCore有大體相同的流程。作出這一改變的緣由爲:(主要動機)減輕機器碼佔用的內存空間,即犧牲時間換空間;提升代碼的啓動速度;對 v8 的代碼進行重構,下降 v8 的代碼複雜度(V8 Ignition:JS 引擎與字節碼的不解之緣 - CNode技術社區)。
JavaScript的性能和C相比還有不小的距離,可預見的將來估計也只能接近它,而不是與它相比,這從語言類型上已經決定。下面將對V8引擎進行更爲細緻的介紹。
V8引擎是一個JavaScript引擎實現,最初由一些語言方面專家設計,後被谷歌收購,隨後谷歌對其進行了開源。V8使用C++開發,,在運行JavaScript以前,相比其它的JavaScript的引擎轉換成字節碼或解釋執行,V8將其編譯成原生機器碼(IA-32, x86-64, ARM, or MIPS CPUs),而且使用瞭如內聯緩存(inline caching)等方法來提升性能。有了這些功能,JavaScript程序在V8引擎下的運行速度媲美二進制程序。V8支持衆多操做系統,如windows、linux、android等,也支持其餘硬件架構,如IA32,X64,ARM等,具備很好的可移植和跨平臺特性。
V8項目代碼結構以下:
JavaScript是一種無類型語言,在編譯時並不能準確知道變量的類型,只能夠在運行時肯定,這就不像c++或者java等靜態類型語言,在編譯時候就能夠確切知道變量的類型。然而,在運行時計算和決定類型,會嚴重影響語言性能,這也就是JavaScript運行效率比C++或者JAVA低不少的緣由之一。
在C++中,源代碼須要通過編譯才能執行,在生成本地代碼的過程當中,變量的地址和類型已經肯定,運行本地代碼時利用數組和位移就能夠存取變量和方法的地址,不須要再進行額外的查找,幾個機器指令便可完成,節省了肯定類型和地址的時間。因爲JavaScript是無類型語言,那就不能像c++那樣在執行時已經知道變量的類型和地址,須要臨時肯定。JavaScript 和C++有如下幾個區別:
在代碼執行過程當中,變量的存取是很是廣泛和頻繁的,經過偏移量來存取,使用少數兩個彙編指令就能完成,若是經過屬性名匹配則須要更多的彙編指令,也須要更多的內存空間。示例以下:
在JavaScript中,除boolean,number,string,null,undefined這個五個簡單變量外,其餘的數據都是對象,V8使用一種特殊的方式來表示它們,進而優化JavaScript的內部表示問題。
在V8中,數據的內部表示由數據的實際內容和數據的句柄構成。數據的實際內容是變長的,類型也是不一樣的;句柄固定大小,包含指向數據的指針。這種設計能夠方便V8進行垃圾回收和移動數據內容,若是直接使用指針的話就會出問題或者須要更大的開銷,使用句柄的話,只需修改句柄中的指針便可,使用者使用的仍是句柄,指針改動是對使用者透明的。
除少數數據(如整型數據)由handle自己存儲外,其餘內容限於句柄大小和變長等緣由,都存儲在堆中。整數直接從value中取值,而後使用一個指針指向它,能夠減小內存的佔用並提升訪問速度。一個句柄對象的大小是4字節(32位設備)或者8字節(64位設備),而在JavaScriptCore中,使用的8個字節表示句柄。在堆中存放的對象都是4字節對齊的,因此它們指針的後兩位是不須要的,V8用這兩位表示數據的類型,00爲整數,01爲其餘。
JavaScript對象在V8中的實現包含三個部分:隱藏類指針,這是v8爲JavaScript對象建立的隱藏類;屬性值表指針,指向該對象包含的屬性值;元素表指針,指向該對象包含的屬性。
前面有過介紹,V8引擎在執行JavaScript的過程當中,主要有兩個階段:編譯和運行,與C++的執行前徹底編譯不一樣的是,JavaScript須要在用戶使用時完成編譯和執行。在V8中,JavaScript相關代碼並不是一下完成編譯的,而是在某些代碼須要執行時,纔會進行編譯,這就提升了響應時間,減小了時間開銷。在V8引擎中,源代碼先被解析器轉變爲抽象語法樹(AST),而後使用JIT編譯器的全代碼生成器從AST直接生成本地可執行代碼。這個過程不一樣於JAVA先生成字節碼或中間表示,減小了AST到字節碼的轉換時間,提升了代碼的執行速度。但因爲缺乏了轉換爲字節碼這一中間過程,也就減小了優化代碼的機會。
V8引擎編譯本地代碼時使用的主要類以下所示:
JavaScript代碼編譯的過程大體爲:Script類調用Compiler類的Compile函數爲其生成本地代碼。Compile函數先使用Parser類生成AST,再使用FullCodeGenerator類來生成本地代碼。本地代碼與具體的硬件平臺密切相關,FullCodeGenerator使用多個後端來生成與平臺相匹配的本地彙編代碼。因爲FullCodeGenerator經過遍歷AST來爲每一個節點生成相應的彙編代碼,缺失了全局視圖,節點之間的優化也就無從談起。
在執行編譯以前,V8會構建衆多全局對象並加載一些內置的庫(如math庫),來構建一個運行環境。並且在JavaScript源代碼中,並不是全部的函數都被編譯生成本地代碼,而是延遲編譯,在調用時纔會編譯。
因爲V8缺乏了生成中間代碼這一環節,缺乏了必要的優化,爲了提高性能,V8會在生成本地代碼後,使用數據分析器(profiler)採集一些信息,而後根據這些數據將本地代碼進行優化,生成更高效的本地代碼,這是一個逐步改進的過程。同時,當發現優化後代碼的性能還不如未優化的代碼,V8將退回原來的代碼,也就是優化回滾。下面介紹一下運行階段,該階段使用的主要類以下所示:
先根據須要編譯和生成這些本地代碼,也就是使用編譯階段那些類和操做。在V8中,函數是一個基本單位,當某個JavaScript函數被調用時,V8會查找該函數是否已經生成本地代碼,若是已經生成,則直接調用該函數。不然,V8引擎會生成屬於該函數的本地代碼。這就節約了時間,減小了處理那些使用不到的代碼的時間。其次,執行編譯後的代碼爲JavaScript構建JS對象,這須要Runtime類來輔組建立對象,並須要從Heap類分配內存。再次,藉助Runtime類中的輔組函數來完成一些功能,如屬性訪問等。最後,將不用的空間進行標記清除和垃圾回收。
由於V8是基於AST直接生成本地代碼,沒有通過中間表示層的優化,因此本地代碼還沒有通過很好的優化。因而,在2010年,V8引入了新的編譯器-Crankshaft,它主要針對熱點函數進行優化,基於JavaScript源代碼開始分析而非本地代碼,同時構建Hydroger圖並基於此來進行優化分析。
Crankshaft編譯器爲了性能考慮,一般會作出比較樂觀和大膽的預測—代碼穩定且變量類型不變,因此能夠生成高效的本地代碼。可是,鑑於JavaScript的一個弱類型的語言,變量類型也可能在執行的過程當中進行改變,鑑於這種狀況,V8會將該編譯器作的想固然的優化進行回滾,稱爲優化回滾。
示例以下:
var counter = 0; function test(x, y) { counter++; if (counter < 1000000) { // do something return 'jeri'; } var unknown = new Date(); console.log(unknown); }
該函數被調用屢次以後,V8引擎可能會觸發Crankshaft編譯器對其進行優化,而優化代碼認爲示例代碼的類型信息都已經被肯定。但,因爲還沒有真正執行到new Date()這個地方,並未獲取unknown這個變量的類型,V8只得將該部分代碼進行回滾。優化回滾是一個很耗時的操做,在寫代碼過程當中,儘可能不要觸發優化該操做。
在最近發佈的 V8 5.9 版本中,新增了一個 Ignition 字節碼解釋器,TurboFan 和 Ignition 結合起來共同完成JavaScript的編譯。這個版本中消除 Cranshaft 這個舊的編譯器,並讓新的 Turbofan 直接從字節碼來優化代碼,並當須要進行反優化的時候直接反優化到字節碼,而不須要再考慮 JS 源代碼。
在執行C++代碼時,僅憑几個指令便可根據偏移信息獲取變量信息,而JavaScript裏須要經過字符串匹配來查找屬性值的,這就須要更多的操做才能訪問到變量信息,而代碼量變量存取是十分頻繁的,這也就制約了JavaScript的性能。V8借用了類和偏移位置的思想,將原本經過屬性名匹配來訪問屬性值的方法進行了改進,使用相似C++編譯器的偏移位置機制來實現,這就是隱藏類。
隱藏類將對象劃分紅不一樣的組,對於組內對象擁有相同的屬性名和屬性值的狀況,將這些組的屬性名和對應的偏移位置保存在一個隱藏類中,組內全部對象共享該信息。同時,也能夠識別屬性不一樣的對象。示例以下:
使用Point構造了兩個對象p和q,這兩個對象具備相同的屬性名,V8將它們歸爲同一個組,也就是隱藏類,這些屬性在隱藏類中有相同的偏移值,p和q共享這一信息,進行屬性訪問時,只需根據隱藏類的偏移值便可。因爲JavaScript是動態類型語言,在執行時能夠更改變量的類型,若是上述代碼執行以後,執行q.z=2,那麼p和q將再也不被認爲是一個組,q將是一個新的隱藏類。
正常訪問對象屬性的過程是:首先獲取隱藏類的地址,而後根據屬性名查找偏移值,而後計算該屬性的地址。雖然相比以往在整個執行環境中查找減少了很大的工做量,但依然比較耗時。能不能將以前查詢的結果緩存起來,供再次訪問呢?固然是可行的,這就是內嵌緩存。
內嵌緩存的大體思路就是將初次查找的隱藏類和偏移值保存起來,當下次查找的時候,先比較當前對象是不是以前的隱藏類,若是是的話,直接使用以前的緩存結果,減小再次查找表的時間。固然,若是一個對象有多個屬性,那麼緩存失誤的機率就會提升,由於某個屬性的類型變化以後,對象的隱藏類也會變化,就與以前的緩存不一致,須要從新使用之前的方式查找哈希表。
Node中經過JavaScript使用內存時就會發現只能使用部份內存(64位系統下約爲1.4 GB,32位系統下約爲0.7 GB),其深層緣由是 V8 垃圾回收機制的限制所致(若是可以使用內存太大,V8在進行垃圾回收時需耗費更多的資源和時間,嚴重影響JS的執行效率)。下面對內存管理進行介紹。
內存的管理組要由分配和回收兩個部分構成。V8的內存劃分以下:
年輕分代:爲新建立的對象分配內存空間,常常須要進行垃圾回收。爲方便年輕分代中的內容回收,可再將年輕分代分爲兩半,一半用來分配,另外一半在回收時負責將以前還須要保留的對象複製過來。
年老分代:根據須要將年老的對象、指針、代碼等數據保存起來,較少地進行垃圾回收。
大對象:爲那些須要使用較多內存對象分配內存,固然一樣可能包含數據和代碼等分配的內存,一個頁面只分配一個對象。
V8 使用了分代和大數據的內存分配,在回收內存時使用精簡整理的算法標記未引用的對象,而後消除沒有標記的對象,最後整理和壓縮那些還未保存的對象,便可完成垃圾回收。
在V8中,使用較多的是年輕分代和年老分代。年輕分代中的對象垃圾回收主要經過Scavenge算法進行垃圾回收。在Scavenge的具體實現中,主要採用了Cheney算法:經過複製的方式實現的垃圾回收算法。它將堆內存分爲兩個 semispace,一個處於使用中(From空間),另外一個處於閒置狀態(To空間)。當分配對象時,先是在From空間中進行分配。當開始進行垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換。在垃圾回收的過程當中,就是經過將存活對象在兩個 semispace 空間之間進行復制。年輕分代中的對象有機會晉升爲年老分代,條件主要有兩個:一個是對象是否經歷過Scavenge回收,一個是To空間的內存佔用比超過限制。
對於年老分代中的對象,因爲存活對象佔較大比重,再採用上面的方式會有兩個問題:一個是存活對象較多,複製存活對象的效率將會很低;另外一個問題依然是浪費一半空間的問題。爲此,V8在年老分代中主要採用了Mark-Sweep(標記清除)標記清除和Mark-Compact(標記整理)相結合的方式進行垃圾回收。
在V8引擎啓動時,須要構建JavaScript運行環境,須要加載不少內置對象,同時也須要創建內置的函數,如Array,String,Math等。爲了使V8更加整潔,加載對象和創建函數等任務都是使用JavaScript文件來實現的,V8引擎負責提供機制來支持,就是在編譯和執行JavaScript前先加載這些文件。
V8引擎須要編譯和執行這些內置的JavaScript代碼,同時使用堆等來保存執行過程當中建立的對象、代碼等,這些都須要時間。爲此,V8引入了快照機制。將這些內置的對象和函數加載以後的內存保存並序列化。序列化以後的結果很容易反序列化,通過快照機制的啓動時間能夠縮減幾毫秒。快照機制也能夠將一些開發者認爲須要的JavaScript文件序列化,以減小處理時間。不過快照機制的加載的代碼不能被CrankShaft這樣的編譯器優化,可能會存在性能問題。
JavaScriptCore引擎是WebKit中默認的JavaScript引擎,也是蘋果開源的一個項目,應用較爲普遍。最初,性能不是很好,從2008年開始了一系列的優化,從新實現了編譯器和字節碼解釋器,使得引擎的性能有較大的提高。隨後內嵌緩存、基於正則表達式的JIT、簡單的JIT及字節碼解釋器等技術引入進來,JavaScriptCore引擎也在不斷的迭代和發展。
V8引擎自誕生之日起就以性能優化做爲目標,引入了衆多新技術,極大了帶動了整個業界JavaScript引擎性能的快速發展。總的來講,V8引擎較爲激進,青睞能夠提升性能的新技術,而JavaScriptCore引擎較爲穩健,漸進式的改變着本身的性能。總的來講JavaScript引擎工做流程(包含v8和JavaScriptCore)以下所示:
JavaScriptCore 的大體流程爲:源代碼-→抽象語法樹-→字節碼-→JIT-→本地代碼。JavaScriptCore與V8有一些不一樣之處,其中最大的不一樣就是新增了字節碼的中間表示,並加入了多層JIT編譯器(如:簡單JIT編譯器、DFG JIT編譯器、LLVM等)優化性能,不停的對本地代碼進行優化。(在 V8 的 5.9 版本中,新增了一個 Ignition 字節碼解釋器,TurboFan 和 Ignition 結合起來共同完成JavaScript的編譯,此後 V8 將與 JavaScriptCore 有大體相同的流程,Node 8.0中 V8 版本爲 5.8)
還有就是在數據表示方面,V8在不一樣的機器上使用與機器位數相匹配的數據表示,而在JavaScriptCore中句柄都是使用64位表示,其能夠表示更大範圍的數字,因此即便在32位機器上,浮點類型一樣能夠保存在句柄中,再也不須要訪問堆中的數據,當也會佔用更多的空間。
JavaScript引擎的主要功能是解析和執行JavaScript代碼,每每不能知足使用者多樣化的須要,那麼就能夠增長擴展以提高它的能力。V8引擎有兩種擴展機制:綁定和擴展。
使用IDL文件或接口文件生成綁定文件,將這些文件同V8引擎一塊兒編譯。WebKit中使用IDL來定義JavaScript,但又與IDL有所不一樣,有一些改變。定義一個新的接口的步驟大體以下:
module mymodule { interface [ InterfaceName = MyObject ] MyObj { readonly attribute long myAttr; DOMString myMethod (DOMString myArg); }; }
JavaScript引擎綁定機制須要將擴展代碼和JavaScript引擎一塊編譯和打包,不能根據須要在引擎啓動後再動態注入這些本地代碼。在實際WEB開發中,開發者都是基於現有瀏覽器的,根本不可能介入到JavaScript引擎的編譯中,綁定機制有很大的侷限性,但其很是高效,適用於對性能要求較高的場景。
經過V8的基類Extension進行能力擴展,無需和V8引擎一塊兒編譯,能夠動態爲引擎增長功能特性,具備很大的靈活性。
Extension機制的大體思路就是,V8提供一個基類Extension和一個全局註冊函數,要想擴展JavaScript能力,須要通過如下步驟:
class MYExtension : public v8::Extension { public: MYExtension() : v8::Extension("v8/My", "native function my();") {} virtual v8::Handle<v8::FunctionTemplate> GetNativeFunction ( v8::Handle<v8::String> name) { // 能夠根據name來返回不一樣的函數 return v8::FunctionTemplate::New(MYExtention::MY); } static v8::Handle<v8::Value> MY(const v8::Arguments& args) { // Do sth here return v8::Undefined(); } }; MYExtension extension; RegisterExtension(&extension);
Extension機制是調用V8的接口注入新函數,動態擴展很是方便,但沒有綁定機制高效,適用於對性能要求不高的場景。
在過去幾年,JavaScript在不少領域獲得了普遍的應用,然而限於JavaScript語言自己的不足,執行效率不高。Google也推出了一些JavaScript網絡應用,如Gmail、Google Maps及Google Docs office等。這些應用的性能不只受到服務器、網絡、渲染引擎以及其餘諸多因素的影響,同時也受到JavaScript自己執行速度的影響。然而既有的JavaScript引擎沒法知足新的需求,而性能不佳一直是網絡應用開發者最關心的。Google就開始了V8引擎的研究,將一系列新技術引入JavaScript引擎中,大大提升了JavaScript的執行效率。相信隨着V8引擎的不斷髮展,JavaScript也會有更普遍的應用場景,前端工程師也會有更好的將來!
那麼結合上面對於V8引擎的介紹,咱們在編程中應注意: