認識V8引擎

JavaScript絕對是最火的編程語言之一,一直具有很大的用戶群,隨著在服務端的使用(NodeJs),更是爆發(fā)了極強的生命力。編程語言分為編譯型語言和解釋型語言兩類,編譯型語言在執(zhí)行之前要先進行完全編譯,而解釋型語言一邊編譯一邊執(zhí)行,很明顯解釋型語言的執(zhí)行速度是慢于編譯型語言的,而JavaScript就是一種解釋型腳本語言,支持動態(tài)類型、弱類型、基于原型的語言,內(nèi)置支持類型。鑒于JavaScript都是在前端執(zhí)行,而且需要及時響應用戶,這就要求JavaScript可以快速的解析及執(zhí)行。
隨著Web相關技術的發(fā)展,JavaScript所要承擔的工作也越來越多,早就超越了“表單驗證”的范疇,這就更需要快速的解析和執(zhí)行JavaScript腳本。V8引擎就是為解決這一問題而生,在node中也是采用該引擎來解析JavaScript。

1.渲染引擎及網(wǎng)頁渲染

1.1.渲染引擎

渲染引擎:能夠?qū)TML/CSS/JavaScript文本及相應的資源文件轉(zhuǎn)換成圖像結果。渲染引擎的主要作用是將資源文件轉(zhuǎn)化為用戶可見的結果。在瀏覽器的發(fā)展過程中,不同的廠商開發(fā)了不同的渲染引擎,如Tridend(IE)、Gecko(FF)、WebKit(Safari,Chrome,Andriod瀏覽器)等。WebKit是由蘋果2005年發(fā)起的一個開源項目,引起了眾多公司的重視,幾年間被很多公司所采用,在移動端更占據(jù)了壟斷地位。更有甚者,開發(fā)出了基于WebKit的支持HTML5的web操作系統(tǒng)(如:Chrome OS、Web OS)。
下面是WebKit的大致結構


v2-959135939fe2cbc2d9a437ef81dff328_b.png

上圖中實線框內(nèi)模塊是所有移植的共有部分,虛線框內(nèi)不同的廠商可以自己實現(xiàn)。下面進行介紹:

  • 操作系統(tǒng):是管理和控制計算機硬件與軟件資源的計算機程序,是直接運行在“裸機”上的最基本的系統(tǒng)軟件,任何其他軟件都必須在操作系統(tǒng)的支持下才能運行。WebKit也是在操作系統(tǒng)上工作的。
  • 第三方庫,為了WebKit提供支持,如圖形庫、網(wǎng)絡庫、視頻庫等。
  • WebCore 是各個瀏覽器使用的共享部分,包括HTML解析器、CSS解析器、DOM和SVG等。JavaScriptCore是WebKit的默認引擎,在谷歌系列產(chǎn)品中被替換為V8引擎。WebKit Ports是WebKit中的非共享部分,由于平臺差異、第三方庫和需求的不同等原因,不同的移植導致了WebKit不同版本行為不一致,它是不同瀏覽器性能和功能差異的關鍵部分。
  • WebKit嵌入式編程接口,供瀏覽器調(diào)用,與移植密切相關,不同的移植有不同的接口規(guī)范。
  • 測試用例,包括布局測試用例和性能測試用例,用來驗證渲染結果的正確性。

1.2.網(wǎng)頁渲染流程

上面介紹了渲染引擎的各個模塊,那么一張網(wǎng)頁,要經(jīng)歷怎樣的過程,才能抵達用戶面前?


v2-ad0a86d3faf223164a9bd22658feadc3_b.png

首先是網(wǎng)頁內(nèi)容,輸入到HTML解析器,HTML解析器解析,然后構建DOM樹,在這期間如果遇到JavaScript代碼則交給JavaScript引擎處理;如果來自CSS解析器的樣式信息,構建一個內(nèi)部繪圖模型。該模型由布局模塊計算模型內(nèi)部各個元素的位置和大小信息,最后由繪圖模塊完成從該模型到圖像的繪制。在網(wǎng)頁渲染的過程中,大致可分為下面3個階段。

1.2.1.從輸入URL到生成DOM樹

1.地址欄輸入URL,WebKit調(diào)用資源加載器加載相應資源;
2.加載器依賴網(wǎng)絡模塊建立連接,發(fā)送請求并接收答復;
3.WebKit接收各種網(wǎng)頁或者資源數(shù)據(jù),其中某些資源可能同步或異步獲取;
4.網(wǎng)頁交給HTML解析器轉(zhuǎn)變?yōu)樵~語;
5.解釋器根據(jù)詞語構建節(jié)點,形成DOM樹;
6.如果節(jié)點是JavaScript代碼,調(diào)用JavaScript引擎解釋并執(zhí)行;
7.JavaScript代碼可能會修改DOM樹結構;
8.如果節(jié)點依賴其他資源,如圖片\css、視頻等,調(diào)用資源加載器加載它們,但這些是異步加載的,不會阻礙當前DOM樹繼續(xù)創(chuàng)建;如果是JavaScript資源URL(沒有標記異步方式),則需要停止當前DOM樹創(chuàng)建,直到JavaScript加載并被JavaScript引擎執(zhí)行后才繼續(xù)DOM樹的創(chuàng)建。

1.2.2.從DOM樹到構建WebKit繪圖上下文

1.CSS文件被CSS解釋器解釋成內(nèi)部表示;
2.CSS解釋器完成工作后,在DOM樹上附加樣式信息,生成RenderObject樹;
3.RenderObject節(jié)點在創(chuàng)建的同時,WebKit會根據(jù)網(wǎng)頁層次結構構建RenderLayer樹,同時構建一個虛擬繪圖上下文。

1.2.3.繪圖上下文到最終圖像呈現(xiàn)

1.繪圖上下文是一個與平臺無關的抽象類,它將每個繪圖操作橋接到不同的具體實現(xiàn)類,也就是繪圖具體實現(xiàn)類;
2.繪圖實現(xiàn)類也可能有簡單的實現(xiàn),也可能有復雜的實現(xiàn),軟件渲染、硬件渲染、合成渲染等;
3.繪圖實現(xiàn)類將2D圖形庫或者3D圖形庫繪制結果保存,交給瀏覽器界面進行展示。
上述是一個完整的渲染過程,現(xiàn)代網(wǎng)頁很多都是動態(tài)的,隨著網(wǎng)頁與用戶的交互,瀏覽器需要不斷的重復渲染過程

1.3.JavaScript引擎

v2-0f5471e21a25e237dcfae2d34a306788_b.png

JavaScript本質(zhì)上是一種解釋型語言,與編譯型語言不同的是它需要一遍執(zhí)行一邊解析,而編譯型語言在執(zhí)行時已經(jīng)完成編譯,可直接執(zhí)行,有更快的執(zhí)行速度(如上圖所示)。JavaScript代碼是在瀏覽器端解析和執(zhí)行的,如果需要時間太長,會影響用戶體驗。那么提高JavaScript的解析速度就是當務之急。JavaScript引擎和渲染引擎的關系如下圖所示:


v2-43b71b75cd4f28db05ab967e3aad5a97_b.png

JavaScript語言是解釋型語言,為了提高性能,引入了Java虛擬機和C++編譯器中的眾多技術。現(xiàn)在JavaScript引擎的執(zhí)行過程大致是:
源代碼-→抽象語法樹-→字節(jié)碼-→JIT-→本地代碼(V8引擎沒有中間字節(jié)碼)。一段代碼的抽象語法樹示例如下:
function demo(name) {
console.log(name);
}


v2-4ee7dc5c0787930c03ff203c41a1482b_b.png

V8更加直接的將抽象語法樹通過JIT技術轉(zhuǎn)換成本地代碼,放棄了在字節(jié)碼階段可以進行的一些性能優(yōu)化,但保證了執(zhí)行速度。在V8生成本地代碼后,也會通過Profiler采集一些信息,來優(yōu)化本地代碼。雖然,少了生成字節(jié)碼這一階段的性能優(yōu)化,但極大減少了轉(zhuǎn)換時間。
但是在2017年4月底,v8 的 5.9 版本發(fā)布了,新增了一個 Ignition 字節(jié)碼解釋器,將默認啟動,從此之后將與JSCore有大致相同的流程。做出這一改變的原因為:(主要動機)減輕機器碼占用的內(nèi)存空間,即犧牲時間換空間;提高代碼的啟動速度;對 v8 的代碼進行重構,降低 v8 的代碼復雜度
JavaScript的性能和C相比還有不小的距離,可預見的未來估計也只能接近它,而不是與它相比,這從語言類型上已經(jīng)決定。下面將對V8引擎進行更為細致的介紹。

2.V8引擎

V8引擎是一個JavaScript引擎實現(xiàn),最初由一些語言方面專家設計,后被谷歌收購,隨后谷歌對其進行了開源。V8使用C++開發(fā),,在運行JavaScript之前,相比其它的JavaScript的引擎轉(zhuǎn)換成字節(jié)碼或解釋執(zhí)行,V8將其編譯成原生機器碼(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如內(nèi)聯(lián)緩存(inline caching)等方法來提高性能。有了這些功能,JavaScript程序在V8引擎下的運行速度媲美二進制程序。V8支持眾多操作系統(tǒng),如windows、linux、android等,也支持其他硬件架構,如IA32,X64,ARM等,具有很好的可移植和跨平臺特性。
V8項目代碼結構如下:


v2-cb5e37b2c362ad0335d6331ef3be6daf_b.png

2.1.數(shù)據(jù)表示

JavaScript是一種無類型語言,在編譯時并不能準確知道變量的類型,只可以在運行時確定,這就不像c++或者java等靜態(tài)類型語言,在編譯時候就可以確切知道變量的類型。然而,在運行時計算和決定類型,會嚴重影響語言性能,這也就是JavaScript運行效率比C++或者JAVA低很多的原因之一。
在C++中,源代碼需要經(jīng)過編譯才能執(zhí)行,在生成本地代碼的過程中,變量的地址和類型已經(jīng)確定,運行本地代碼時利用數(shù)組和位移就可以存取變量和方法的地址,不需要再進行額外的查找,幾個機器指令即可完成,節(jié)省了確定類型和地址的時間。由于JavaScript是無類型語言,那就不能像c++那樣在執(zhí)行時已經(jīng)知道變量的類型和地址,需要臨時確定。JavaScript 和C++有以下幾個區(qū)別:

  • 編譯確定位置,C++編譯階段確定位置偏移信息,在執(zhí)行時直接存取,JavaScript在執(zhí)行階段確定,而且執(zhí)行期間可以修改對象屬性;
  • 偏移信息共享,C++有類型定義,執(zhí)行時不能動態(tài)改變,可共享偏移信息,JavaScript每個對象都是自描述,屬性和位置偏移信息都包含在自身的結構中;
  • 偏移信息查找,C++查找偏移地址很簡單,在編譯代碼階段,對使用的某類型成員變量直接設置偏移位置,JavaScript中使用一個對象,需要通過屬性名匹配才能找到相應的值,需要更多的操作。
    在代碼執(zhí)行過程中,變量的存取是非常普遍和頻繁的,通過偏移量來存取,使用少數(shù)兩個匯編指令就能完成,如果通過屬性名匹配則需要更多的匯編指令,也需要更多的內(nèi)存空間。示例如下:


    v2-794bcbcc25818b827d77ff712271ce56_b.jpg

    在JavaScript中,除boolean,number,string,null,undefined這個五個簡單變量外,其他的數(shù)據(jù)都是對象,V8使用一種特殊的方式來表示它們,進而優(yōu)化JavaScript的內(nèi)部表示問題。
    在V8中,數(shù)據(jù)的內(nèi)部表示由數(shù)據(jù)的實際內(nèi)容和數(shù)據(jù)的句柄構成。數(shù)據(jù)的實際內(nèi)容是變長的,類型也是不同的;句柄固定大小,包含指向數(shù)據(jù)的指針。這種設計可以方便V8進行垃圾回收和移動數(shù)據(jù)內(nèi)容,如果直接使用指針的話就會出問題或者需要更大的開銷,使用句柄的話,只需修改句柄中的指針即可,使用者使用的還是句柄,指針改動是對使用者透明的。
    除少數(shù)數(shù)據(jù)(如整型數(shù)據(jù))由handle本身存儲外,其他內(nèi)容限于句柄大小和變長等原因,都存儲在堆中。整數(shù)直接從value中取值,然后使用一個指針指向它,可以減少內(nèi)存的占用并提高訪問速度。一個句柄對象的大小是4字節(jié)(32位設備)或者8字節(jié)(64位設備),而在JavaScriptCore中,使用的8個字節(jié)表示句柄。在堆中存放的對象都是4字節(jié)對齊的,所以它們指針的后兩位是不需要的,V8用這兩位表示數(shù)據(jù)的類型,00為整數(shù),01為其他。
    JavaScript對象在V8中的實現(xiàn)包含三個部分:隱藏類指針,這是v8為JavaScript對象創(chuàng)建的隱藏類;屬性值表指針,指向該對象包含的屬性值;元素表指針,指向該對象包含的屬性。

2.2.工作過程

前面有過介紹,V8引擎在執(zhí)行JavaScript的過程中,主要有兩個階段:編譯和運行,與C++的執(zhí)行前完全編譯不同的是,JavaScript需要在用戶使用時完成編譯和執(zhí)行。在V8中,JavaScript相關代碼并非一下完成編譯的,而是在某些代碼需要執(zhí)行時,才會進行編譯,這就提高了響應時間,減少了時間開銷。在V8引擎中,源代碼先被解析器轉(zhuǎn)變?yōu)槌橄笳Z法樹(AST),然后使用JIT編譯器的全代碼生成器從AST直接生成本地可執(zhí)行代碼。這個過程不同于JAVA先生成字節(jié)碼或中間表示,減少了AST到字節(jié)碼的轉(zhuǎn)換時間,提高了代碼的執(zhí)行速度。但由于缺少了轉(zhuǎn)換為字節(jié)碼這一中間過程,也就減少了優(yōu)化代碼的機會。
V8引擎編譯本地代碼時使用的主要類如下所示:

  • Script:表示JavaScript代碼,即包含源代碼,又包含編譯之后生成的本地代碼,即是編譯入口,又是運行入口;
  • Compiler:編譯器類,輔組Script類來編譯生成代碼,調(diào)用解釋器(Parser)來生成AST和全代碼生成器,將AST轉(zhuǎn)變?yōu)楸镜卮a;
  • AstNode:抽象語法樹節(jié)點類,是其他所有節(jié)點的基類,包含非常多的子類,后面會針對不同的子類生成不同的本地代碼;
  • AstVisitor:抽象語法樹的訪問者類,主要用來遍歷異構的抽象語法樹;
  • FullCodeGenerator:AstVisitor類的子類,通過遍歷AST來為JavaScript生成本地可執(zhí)行代碼。


    v2-274bcd0e43b8b718ce58de8ae51e1b70_b.png

    JavaScript代碼編譯的過程大致為:Script類調(diào)用Compiler類的Compile函數(shù)為其生成本地代碼。Compile函數(shù)先使用Parser類生成AST,再使用FullCodeGenerator類來生成本地代碼。本地代碼與具體的硬件平臺密切相關,F(xiàn)ullCodeGenerator使用多個后端來生成與平臺相匹配的本地匯編代碼。由于FullCodeGenerator通過遍歷AST來為每個節(jié)點生成相應的匯編代碼,缺失了全局視圖,節(jié)點之間的優(yōu)化也就無從談起。
    在執(zhí)行編譯之前,V8會構建眾多全局對象并加載一些內(nèi)置的庫(如math庫),來構建一個運行環(huán)境。而且在JavaScript源代碼中,并非所有的函數(shù)都被編譯生成本地代碼,而是延遲編譯,在調(diào)用時才會編譯。
    由于V8缺少了生成中間代碼這一環(huán)節(jié),缺少了必要的優(yōu)化,為了提升性能,V8會在生成本地代碼后,使用數(shù)據(jù)分析器(profiler)采集一些信息,然后根據(jù)這些數(shù)據(jù)將本地代碼進行優(yōu)化,生成更高效的本地代碼,這是一個逐步改進的過程。同時,當發(fā)現(xiàn)優(yōu)化后代碼的性能還不如未優(yōu)化的代碼,V8將退回原來的代碼,也就是優(yōu)化回滾。下面介紹一下運行階段,該階段使用的主要類如下所示:

  • Script:表示JavaScript代碼,即包含源代碼,又包含編譯之后生成的本地代碼,即是編譯入口,又是運行入口;
  • Execution:運行代碼的輔組類,包含一些重要函數(shù),如Call函數(shù),它輔組進入和執(zhí)行Script代碼;
  • JSFunction:需要執(zhí)行的JavaScript函數(shù)表示類;
  • Runtime:運行這些本地代碼的輔組類,主要提供運行時所需的輔組函數(shù),如:屬性訪問、類型轉(zhuǎn)換、編譯、算術、位操作、比較、正則表達式等;
  • Heap:運行本地代碼需要使用的內(nèi)存堆類;
  • MarkCompactCollector:垃圾回收機制的主要實現(xiàn)類,用來標記、清除和整理等基本的垃圾回收過程;
  • SweeperThread:負責垃圾回收的線程。


    v2-d9eaec9f9912dd0e26c636675fe10fe3_b.png

    先根據(jù)需要編譯和生成這些本地代碼,也就是使用編譯階段那些類和操作。在V8中,函數(shù)是一個基本單位,當某個JavaScript函數(shù)被調(diào)用時,V8會查找該函數(shù)是否已經(jīng)生成本地代碼,如果已經(jīng)生成,則直接調(diào)用該函數(shù)。否則,V8引擎會生成屬于該函數(shù)的本地代碼。這就節(jié)約了時間,減少了處理那些使用不到的代碼的時間。其次,執(zhí)行編譯后的代碼為JavaScript構建JS對象,這需要Runtime類來輔組創(chuàng)建對象,并需要從Heap類分配內(nèi)存。再次,借助Runtime類中的輔組函數(shù)來完成一些功能,如屬性訪問等。最后,將不用的空間進行標記清除和垃圾回收

2.3.優(yōu)化回滾

因為V8是基于AST直接生成本地代碼,沒有經(jīng)過中間表示層的優(yōu)化,所以本地代碼尚未經(jīng)過很好的優(yōu)化。于是,在2010年,V8引入了新的編譯器-Crankshaft,它主要針對熱點函數(shù)進行優(yōu)化,基于JavaScript源代碼開始分析而非本地代碼,同時構建Hydroger圖并基于此來進行優(yōu)化分析。
Crankshaft編譯器為了性能考慮,通常會做出比較樂觀和大膽的預測—代碼穩(wěn)定且變量類型不變,所以可以生成高效的本地代碼。但是,鑒于JavaScript的一個弱類型的語言,變量類型也可能在執(zhí)行的過程中進行改變,鑒于這種情況,V8會將該編譯器做的想當然的優(yōu)化進行回滾,稱為優(yōu)化回滾。
示例如下:
var counter = 0;
function test(x, y) {
counter++;
if (counter < 1000000) {
// do something
return 'jeri';
}
var unknown = new Date();
console.log(unknown);
}
該函數(shù)被調(diào)用多次之后,V8引擎可能會觸發(fā)Crankshaft編譯器對其進行優(yōu)化,而優(yōu)化代碼認為示例代碼的類型信息都已經(jīng)被確定。但,由于尚未真正執(zhí)行到new Date()這個地方,并未獲取unknown這個變量的類型,V8只得將該部分代碼進行回滾。優(yōu)化回滾是一個很耗時的操作,在寫代碼過程中,盡量不要觸發(fā)優(yōu)化該操作。
在最近發(fā)布的 V8 5.9 版本中,新增了一個 Ignition 字節(jié)碼解釋器,TurboFan 和 Ignition 結合起來共同完成JavaScript的編譯。這個版本中消除 Cranshaft 這個舊的編譯器,并讓新的 Turbofan 直接從字節(jié)碼來優(yōu)化代碼,并當需要進行反優(yōu)化的時候直接反優(yōu)化到字節(jié)碼,而不需要再考慮 JS 源代碼。

2.4.隱藏類與內(nèi)嵌緩存

2.4.1.隱藏類

在執(zhí)行C++代碼時,僅憑幾個指令即可根據(jù)偏移信息獲取變量信息,而JavaScript里需要通過字符串匹配來查找屬性值的,這就需要更多的操作才能訪問到變量信息,而代碼量變量存取是十分頻繁的,這也就制約了JavaScript的性能。V8借用了類和偏移位置的思想,將本來通過屬性名匹配來訪問屬性值的方法進行了改進,使用類似C++編譯器的偏移位置機制來實現(xiàn),這就是隱藏類。
隱藏類將對象劃分成不同的組,對于組內(nèi)對象擁有相同的屬性名和屬性值的情況,將這些組的屬性名和對應的偏移位置保存在一個隱藏類中,組內(nèi)所有對象共享該信息。同時,也可以識別屬性不同的對象。示例如下:


v2-bf67f5eb2667ddd86bfde785612aca35_b.png

使用Point構造了兩個對象p和q,這兩個對象具有相同的屬性名,V8將它們歸為同一個組,也就是隱藏類,這些屬性在隱藏類中有相同的偏移值,p和q共享這一信息,進行屬性訪問時,只需根據(jù)隱藏類的偏移值即可。由于JavaScript是動態(tài)類型語言,在執(zhí)行時可以更改變量的類型,如果上述代碼執(zhí)行之后,執(zhí)行q.z=2,那么p和q將不再被認為是一個組,q將是一個新的隱藏類。

2.4.2.內(nèi)嵌緩存

正常訪問對象屬性的過程是:首先獲取隱藏類的地址,然后根據(jù)屬性名查找偏移值,然后計算該屬性的地址。雖然相比以往在整個執(zhí)行環(huán)境中查找減小了很大的工作量,但依然比較耗時。能不能將之前查詢的結果緩存起來,供再次訪問呢?當然是可行的,這就是內(nèi)嵌緩存。
內(nèi)嵌緩存的大致思路就是將初次查找的隱藏類和偏移值保存起來,當下次查找的時候,先比較當前對象是否是之前的隱藏類,如果是的話,直接使用之前的緩存結果,減少再次查找表的時間。當然,如果一個對象有多個屬性,那么緩存失誤的概率就會提高,因為某個屬性的類型變化之后,對象的隱藏類也會變化,就與之前的緩存不一致,需要重新使用以前的方式查找哈希表。

2.5.內(nèi)存管理

Node中通過JavaScript使用內(nèi)存時就會發(fā)現(xiàn)只能使用部分內(nèi)存(64位系統(tǒng)下約為1.4 GB,32位系統(tǒng)下約為0.7 GB),其深層原因是 V8 垃圾回收機制的限制所致(如果可使用內(nèi)存太大,V8在進行垃圾回收時需耗費更多的資源和時間,嚴重影響JS的執(zhí)行效率)。下面對內(nèi)存管理進行介紹。
內(nèi)存的管理組要由分配和回收兩個部分構成。V8的內(nèi)存劃分如下:

  • Zone:管理小塊內(nèi)存。其先自己申請一塊內(nèi)存,然后管理和分配一些小內(nèi)存,當一塊小內(nèi)存被分配之后,不能被Zone回收,只能一次性回收Zone分配的所有小內(nèi)存。當一個過程需要很多內(nèi)存,Zone將需要分配大量的內(nèi)存,卻又不能及時回收,會導致內(nèi)存不足情況。
  • 堆:管理JavaScript使用的數(shù)據(jù)、生成的代碼、哈希表等。為方便實現(xiàn)垃圾回收,堆被分為三個部分:
    年輕分代:為新創(chuàng)建的對象分配內(nèi)存空間,經(jīng)常需要進行垃圾回收。為方便年輕分代中的內(nèi)容回收,可再將年輕分代分為兩半,一半用來分配,另一半在回收時負責將之前還需要保留的對象復制過來。
    年老分代:根據(jù)需要將年老的對象、指針、代碼等數(shù)據(jù)保存起來,較少地進行垃圾回收。
    大對象:為那些需要使用較多內(nèi)存對象分配內(nèi)存,當然同樣可能包含數(shù)據(jù)和代碼等分配的內(nèi)存,一個頁面只分配一個對象。


    v2-76f0fb426e86e9b54fecb6bef2582e8b_b.png

垃圾回收

V8 使用了分代和大數(shù)據(jù)的內(nèi)存分配,在回收內(nèi)存時使用精簡整理的算法標記未引用的對象,然后消除沒有標記的對象,最后整理和壓縮那些還未保存的對象,即可完成垃圾回收。
在V8中,使用較多的是年輕分代和年老分代。年輕分代中的對象垃圾回收主要通過Scavenge算法進行垃圾回收。在Scavenge的具體實現(xiàn)中,主要采用了Cheney算法:通過復制的方式實現(xiàn)的垃圾回收算法。它將堆內(nèi)存分為兩個 semispace,一個處于使用中(From空間),另一個處于閑置狀態(tài)(To空間)。當分配對象時,先是在From空間中進行分配。當開始進行垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被復制到To空間中,而非存活對象占用的空間將會被釋放。完成復制后,F(xiàn)rom空間和To空間的角色發(fā)生對換。在垃圾回收的過程中,就是通過將存活對象在兩個 semispace 空間之間進行復制。年輕分代中的對象有機會晉升為年老分代,條件主要有兩個:一個是對象是否經(jīng)歷過Scavenge回收,一個是To空間的內(nèi)存占用比超過限制。
對于年老分代中的對象,由于存活對象占較大比重,再采用上面的方式會有兩個問題:一個是存活對象較多,復制存活對象的效率將會很低;另一個問題依然是浪費一半空間的問題。為此,V8在年老分代中主要采用了Mark-Sweep(標記清除)標記清除和Mark-Compact(標記整理)相結合的方式進行垃圾回收。

2.6.快照

在V8引擎啟動時,需要構建JavaScript運行環(huán)境,需要加載很多內(nèi)置對象,同時也需要建立內(nèi)置的函數(shù),如Array,String,Math等。為了使V8更加整潔,加載對象和建立函數(shù)等任務都是使用JavaScript文件來實現(xiàn)的,V8引擎負責提供機制來支持,就是在編譯和執(zhí)行JavaScript前先加載這些文件。
V8引擎需要編譯和執(zhí)行這些內(nèi)置的JavaScript代碼,同時使用堆等來保存執(zhí)行過程中創(chuàng)建的對象、代碼等,這些都需要時間。為此,V8引入了快照機制。將這些內(nèi)置的對象和函數(shù)加載之后的內(nèi)存保存并序列化。序列化之后的結果很容易反序列化,經(jīng)過快照機制的啟動時間可以縮減幾毫秒。快照機制也可以將一些開發(fā)者認為需要的JavaScript文件序列化,以減少處理時間。不過快照機制的加載的代碼不能被CrankShaft這樣的編譯器優(yōu)化,可能會存在性能問題。

3.V8 VS JavaScriptCore

JavaScriptCore引擎是WebKit中默認的JavaScript引擎,也是蘋果開源的一個項目,應用較為廣泛。最初,性能不是很好,從2008年開始了一系列的優(yōu)化,重新實現(xiàn)了編譯器和字節(jié)碼解釋器,使得引擎的性能有較大的提升。隨后內(nèi)嵌緩存、基于正則表達式的JIT、簡單的JIT及字節(jié)碼解釋器等技術引入進來,JavaScriptCore引擎也在不斷的迭代和發(fā)展。
V8引擎自誕生之日起就以性能優(yōu)化作為目標,引入了眾多新技術,極大了帶動了整個業(yè)界JavaScript引擎性能的快速發(fā)展。總的來說,V8引擎較為激進,青睞可以提高性能的新技術,而JavaScriptCore引擎較為穩(wěn)健,漸進式的改變著自己的性能。總的來說JavaScript引擎工作流程(包含v8和JavaScriptCore)如下所示:


v2-993bbf0a46f6feaef5f7b5005aa1aa34_b.png

JavaScriptCore 的大致流程為:源代碼-→抽象語法樹-→字節(jié)碼-→JIT-→本地代碼。JavaScriptCore與V8有一些不同之處,其中最大的不同就是新增了字節(jié)碼的中間表示,并加入了多層JIT編譯器(如:簡單JIT編譯器、DFG JIT編譯器、LLVM等)優(yōu)化性能,不停的對本地代碼進行優(yōu)化。(在 V8 的 5.9 版本中,新增了一個 Ignition 字節(jié)碼解釋器,TurboFan 和 Ignition 結合起來共同完成JavaScript的編譯,此后 V8 將與 JavaScriptCore 有大致相同的流程,Node 8.0中 V8 版本為 5.8)
還有就是在數(shù)據(jù)表示方面,V8在不同的機器上使用與機器位數(shù)相匹配的數(shù)據(jù)表示,而在JavaScriptCore中句柄都是使用64位表示,其可以表示更大范圍的數(shù)字,所以即使在32位機器上,浮點類型同樣可以保存在句柄中,不再需要訪問堆中的數(shù)據(jù),當也會占用更多的空間。

4.功能擴展

JavaScript引擎的主要功能是解析和執(zhí)行JavaScript代碼,往往不能滿足使用者多樣化的需要,那么就可以增加擴展以提升它的能力。V8引擎有兩種擴展機制:綁定和擴展。

4.1.綁定機制

使用IDL文件或接口文件生成綁定文件,將這些文件同V8引擎一起編譯。WebKit中使用IDL來定義JavaScript,但又與IDL有所不同,有一些改變。定義一個新的接口的步驟大致如下:

  • 1.定義新的接口文件,可以在JavaScript代碼進行調(diào)用,如mymodule.MyObj.myAttr;
    module mymodule {
    interface [
    InterfaceName = MyObject
    ] MyObj {
    readonly attribute long myAttr;
    DOMString myMethod (DOMString myArg);
    };
    }
  • 2.按照引擎定義的標準接口為基礎實現(xiàn)接口類,生成JavaScript引擎所需的綁定文件。WebKit提供了工具幫助生成所需的綁定類,根據(jù)引擎不同和引擎開發(fā)語言的不同而有所差異。V8引擎會為上述示例代碼生成 v8MyObj.h (MyObj類具體的實現(xiàn)代碼)和 V8MyObj.cpp (橋接代碼,輔組注冊橋接的函數(shù)到V8引擎)兩個綁定文件。
    JavaScript引擎綁定機制需要將擴展代碼和JavaScript引擎一塊編譯和打包,不能根據(jù)需要在引擎啟動后再動態(tài)注入這些本地代碼。在實際WEB開發(fā)中,開發(fā)者都是基于現(xiàn)有瀏覽器的,根本不可能介入到JavaScript引擎的編譯中,綁定機制有很大的局限性,但其非常高效,適用于對性能要求較高的場景

4.2. Extension機制

通過V8的基類Extension進行能力擴展,無需和V8引擎一起編譯,可以動態(tài)為引擎增加功能特性,具有很大的靈活性。
Extension機制的大致思路就是,V8提供一個基類Extension和一個全局注冊函數(shù),要想擴展JavaScript能力,需要經(jīng)過以下步驟:
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) {
// 可以根據(jù)name來返回不同的函數(shù)
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);

  • 1.基于Extension基類構建一個它的子類,并實現(xiàn)它的虛函數(shù)—GetNativeFunction,根據(jù)參數(shù)name來決定返回實函數(shù);
  • 2.創(chuàng)建一個該子類的對象,并通過注冊函數(shù)將該對象注冊到V8引擎,當JavaScript調(diào)用’my’函數(shù)時就可被調(diào)用到。
    Extension機制是調(diào)用V8的接口注入新函數(shù),動態(tài)擴展非常方便,但沒有綁定機制高效,適用于對性能要求不高的場景。

總結

在過去幾年,JavaScript在很多領域得到了廣泛的應用,然而限于JavaScript語言本身的不足,執(zhí)行效率不高。Google也推出了一些JavaScript網(wǎng)絡應用,如Gmail、Google Maps及Google Docs office等。這些應用的性能不僅受到服務器、網(wǎng)絡、渲染引擎以及其他諸多因素的影響,同時也受到JavaScript本身執(zhí)行速度的影響。然而既有的JavaScript引擎無法滿足新的需求,而性能不佳一直是網(wǎng)絡應用開發(fā)者最關心的。Google就開始了V8引擎的研究,將一系列新技術引入JavaScript引擎中,大大提高了JavaScript的執(zhí)行效率。相信隨著V8引擎的不斷發(fā)展,JavaScript也會有更廣泛的應用場景,前端工程師也會有更好的未來!
那么結合上面對于V8引擎的介紹,我們在編程中應注意:

  • 類型。對于函數(shù),JavaScript是一種動態(tài)類型語言,JavaScriptCore和V8都使用隱藏類和內(nèi)嵌緩存來提高性能,為了保證緩存命中率,一個函數(shù)應該使用較少的數(shù)據(jù)類型;對于數(shù)組,應盡量存放相同類型的數(shù)據(jù),這樣就可以通過偏移位置來訪問。
  • 數(shù)據(jù)表示。簡單類型數(shù)據(jù)(如整型)直接保存在句柄中,可以減少尋址時間和內(nèi)存占用,如果可以使用整數(shù)表示的,盡量不要用浮點類型。
  • 內(nèi)存。雖然JavaScript語言會自己進行垃圾回收,但我們也應盡量做到及時回收不用的內(nèi)存,對不再使用的對象設置為null或使用delete方法來刪除(使用delete方法刪除會觸發(fā)隱藏類新建,需要更多的額外操作)。
  • 優(yōu)化回滾。在執(zhí)行多次之后,不要出現(xiàn)修改對象類型的語句,盡量不要觸發(fā)優(yōu)化回滾,否則會大幅度降低代碼的性能。
  • 新機制。使用JavaScript引擎或者渲染引擎提供的新機制和新接口提高性能。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,623評論 2 370

推薦閱讀更多精彩內(nèi)容