本系列的第一篇文章重點介紹了引擎,運行時和調用棧的概述。第二篇文章將深刻V8的JavaScript引擎的內部。咱們還會提供一些關於如何編寫更好的JavaScript代碼的技巧。編程
JavaScript引擎是執行JavaScript代碼的程序或解釋器。JavaScript引擎能夠用標準解釋器(interpreter)或即時編譯器(just-in-time compiler)來實現,即時編譯器以某種形式將JavaScript代碼編譯爲字節碼。segmentfault
流行的JavaScript引擎:數組
V8引擎是由Google構建的,用C++開發而且開源,與其它的引擎不一樣的是,V8仍是Node.js的運行時環境。瀏覽器
V8最初設計用於提升瀏覽器內部JavaScript執行的性能。爲了得到速度,V8將JavaScript代碼轉換爲更高效的機器代碼(machine code),而不是使用解釋器。它經過實現JIT(Just-In-Time)編譯器(如SpiderMonkey或Rhino,等許多現代JavaScript引擎)將JavaScript代碼編譯爲機器代碼。這裏的主要區別在於V8不生成字節碼或任何中間代碼。緩存
在V8引擎的v5.9版本出來以前,V8有兩個編譯器:
full-codegen:一個簡單並且速度很是快的編譯器,能夠生成簡單且相對較慢的機器代碼。
Crankshaft:一種更復雜(Just-In-Time)的優化編譯器,能夠生成高度優化的代碼。架構
V8引擎還在內部使用多個線程:編程語言
當第一次執行JavaScript代碼時,V8利用full-codegen,直接將解析的JavaScript翻譯成機器代碼而無需任何轉換。這使它能夠很是快速地開始執行機器代碼。請注意,V8不使用中間字節碼錶示法,不須要解釋器。ide
當您的代碼運行一段時間後,Profiler線程已經收集了足夠的數據以肯定哪一種方法應該進行優化。函數
接下來,Crankshaft優化從另外一個線程開始。它將JavaScript抽象語法樹翻譯爲稱爲Hydrogen的高級靜態單分配(SSA)表示,並嘗試優化該hydrogen圖。大多數優化都是在這個級別完成的。佈局
第一次優化是提早儘量多地嵌入代碼。 內聯是將被調用函數的主體替換爲調用網站(調用該函數的代碼行)的過程。 這個簡單的步驟可讓如下優化變得更有意義。
JavaScript是一種基於原型的語言:沒有類,對象的建立是經過克隆實現的。JavaScript也是一種動態編程語言,它意味着屬性能夠在實例化後輕鬆添加或從對象中移除。
大多數JavaScript解釋器使用字典式結構(基於哈希函數)來存儲對象屬性值在內存中的位置。這種結構使得檢索JavaScript中的屬性的值比在Java或C#等非動態編程語言中的計算更昂貴。在Java中,全部對象屬性都是在編譯以前由固定的對象佈局肯定的,而且不能在運行時動態添加或刪除(固然,C#的動態類型是另外一個主題)。所以,屬性的值(或指向這些屬性的指針)能夠做爲連續緩衝區存儲在內存中,每一個值之間都有一個固定偏移量。偏移量的長度能夠根據屬性類型輕鬆肯定,但在運行時能夠更改屬性類型的JavaScript中不可行。
因爲使用字典查找內存中對象屬性的位置效率很是低,所以V8使用不一樣的方法:隱藏類。隱藏類的工做方式與Java等語言中使用的固定對象佈局(類)相似,除了它們是在運行時建立的。如今,讓咱們看看他們實際的樣子:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
當「new Point(1, 2)」被執行時, V8引擎會建立一個名爲C0的隱藏類。
因爲Point還未定義任何屬性,所以「C0」爲空。
一旦執行了第一條語句「this.x = x」(在「Point」函數內部),V8將建立第二個隱藏類「C1」,它基於「C0」。「C1」描述了能夠找到屬性x的存儲器中的位置(相對於對象指針)。在這種狀況下,「x」存儲在偏移量0處,這意味着在內存中將點對象視爲連續緩衝區時,第一個偏移量將對應於屬性「x」。 V8還將用「類別轉換」更新「C0」,該類別轉換指出若是將屬性「x」添加到點對象,隱藏類應從「C0」切換到「C1」。 下面的點對象的隱藏類如今是「C1」。
每次將新屬性添加到對象時,舊的隱藏類都會使用到新隱藏類的轉換路徑進行更新。隱藏類轉換很是重要,由於它們容許隱藏類在以相同方式建立的對象之間共享。若是兩個對象共享一個隱藏類並向它們添加了相同的屬性,則轉換將確保兩個對象都接收到相同的新隱藏類以及隨附的全部優化代碼。
當執行語句「this.y = y」(一樣,在「this.x = x」語句以後的Point函數內部)時,將重複此過程。
建立一個名爲「C2」的新隱藏類,將類轉換添加到「C1」,指出若是將屬性「y」添加到Point對象(已包含屬性「x」),則隱藏類應更改成 「C2」,點對象的隱藏類更新爲「C2」。
隱藏類轉換取決於將屬性添加到對象的順序。 看看下面的代碼片斷:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 5); p2.b = 7; p2.a = 8;
如今,您可能認爲對於p1和p2,將使用相同的隱藏類和轉換。事實上卻不是。對於「p1」,首先添加屬性「a」,而後添加屬性「b」。然而,對於「p2」,首先分配「b」,而後是「a」。 所以,因爲不一樣的轉換路徑,「p1」和「p2」以不一樣的隱藏類結束。在這種狀況下,以相同順序初始化動態屬性好得多,以便隱藏的類能夠重用。
V8利用另外一種技術來優化稱爲內聯緩存的動態類型化語言。內聯緩存依賴於觀察到對相同方法的重複調用傾向於發生在相同類型的對象上。在這裏能夠找到關於內聯緩存的深刻解釋。
咱們將討論內聯緩存的通常概念(若是您沒有時間經過上面的深刻解釋)。
那麼它是怎樣工做的? V8維護一個對象類型的緩存,這些對象在最近的方法調用中做爲參數傳遞,並使用這些信息來預測未來做爲參數傳遞的對象的類型。若是V8可以對傳遞給方法的對象的類型作出很好的假設,那麼它能夠繞過肯定如何訪問對象屬性的過程,而是使用之前查找存儲的信息到對象的隱藏課程。
那麼隱藏類和內聯緩存的概念如何相關?不管什麼時候在特定對象上調用方法,V8引擎都必須執行對該對象的隱藏類的查找,以肯定訪問特定屬性的偏移量。在相同隱藏類的兩次成功調用以後,V8省略了隱藏類查找,並簡單地將該屬性的偏移量添加到對象指針自己。對於該方法的全部將來調用,V8引擎都假定隱藏的類沒有更改,並使用從之前的查找存儲的偏移量直接跳轉到特定屬性的內存地址。這大大提升了執行速度。
內聯緩存也是爲何相同類型的對象共享隱藏類很是重要的緣由。若是您建立兩個具備相同類型和不一樣隱藏類的對象(就像咱們以前的示例中那樣),V8將沒法使用內聯緩存,由於即便這兩個對象的類型相同,它們對應的隱藏類爲其屬性分配不一樣的偏移量。
一旦Hydrogen圖被優化,Crankshaft將其下降到稱爲Lithium的較低級表示。大部分的Lithium實施都是特定於架構的。寄存器分配發生在這個級別。
最終,Lithium被編譯成機器碼。而後發生其餘事情,稱爲OSR:堆棧替換。在咱們開始編譯和優化那些耗時較長的方法以前,咱們可能會運行它。V8不會忘記它剛剛緩慢執行的內容,以再次優化版本開始。相反,它會轉換咱們擁有的全部上下文(堆棧,寄存器),以便咱們能夠在執行過程當中切換到優化版本。這是一項很是複雜的任務,考慮到除了其餘優化以外,V8最初仍是將代碼內聯。 V8不是惟一可以作到的引擎。
有一種叫作去最佳化的保護措施能夠作出相反的轉變,並在引擎的假設再也不成立的狀況下恢復到非優化的代碼。
對於垃圾收集,V8採用了傳統的標記清除方式來清理老一代。標記階段應該中止JavaScript執行。爲了控制GC成本並使執行更加穩定,V8使用增量標記:不是遍歷整個堆,而是試圖標記每一個可能的對象,它只走過堆的一部分,而後恢復正常執行。下一個GC中止將從先前堆走過的地方繼續。這容許在正常執行期間很是短的暫停。如前所述,掃描階段由單獨的線程處理。
隨着2017年早些時候發佈V8 5.9,引入了新的執行流程。這個新的管道在實際的JavaScript應用程序中實現了更大的性能改進和顯着的內存節省。
新的執行流程創建在Ignition,V8的解釋器和TurboFan,V8的最新優化編譯器之上。
您能夠查看V8團隊關於此主題的博客文章。
自從V8.5版本問世以來,V8團隊一直在努力跟上新的JavaScript語言特性,而V8團隊已經再也不使用V8版本的full-codegen和Crankshaft(自2010年以來服務於V8的技術)。這些功能須要進行優化。
這意味着總體V8將有更簡單和更可維護的架構。
這些改進僅僅是一個開始。 新的Ignition和TurboFan管道爲進一步優化鋪平了道路,這將在將來幾年提高JavaScript性能並縮小V8在Chrome和Node.js中的佔用空間。
最後,這裏有一些關於如何編寫優化的,更好的JavaScript的技巧和竅門。 您能夠輕鬆地從上述內容中獲取這些內容,可是,爲了方便起見,如下是摘要: