幾周前,我們開始寫旨在深入挖掘JavaScript及其工作機制的一系列文章:我們認為,通過了解JavaScript的構造單元以及這些構造單元如何組織在一起,您就能夠編寫更好的代碼和應用程序。
該系列的第一篇文章重點是提供一個對引擎、運行時和調用棧的概述。這第二篇文章將會深入Google V8 JavaScript引擎的內部。我們還將提供如何編寫更佳 JavaScript 代碼的一些小技巧 - 這也是我們 SessionStack 開發團隊在構建產品時遵循的最佳實踐。
概述
JavaScript引擎是一個執行JavaScript代碼的程序或解釋器。JavaScript引擎可以被實現為標準解釋器,或者實現為以某種形式將JavaScript編譯為字節碼的即時編譯器。
下面是實現了JavaScript引擎的一個熱門項目列表:
V8 —?開源,由Google開發,用C++編寫的
Rhino —?由Mozilla基金所管理,開源,完全用Java開發
SpiderMonkey —第一個JavaScript引擎,最早用在Netscape Navigator上,現在用在Firefox上。
JavaScriptCore —?開源,以Nitro銷售,由蘋果公司為Safari開發
KJS —KDE的引擎最初由Harri Porten開發,用于KDE項目的Konqueror瀏覽器
Chakra (JScript9) —?Internet Explorer
Chakra (JavaScript) —?Microsoft Edge
Nashorn— 開源為OpenJDK的一部分,由Oracle的Java語言和工具組開發
JerryScript —? 是用于物聯網的輕量級引擎
創建V8引擎的由來
Google構建的V8引擎是開源的,用C++編寫的。該引擎被用在Google Chrome中。不過,與其他引擎不同的是,V8還被用作很受歡迎的Node.js的運行時。
V8最初是設計用來提升Web瀏覽器中JavaScript執行的性能。為了獲得速度,V8將JavaScript代碼轉換為更高效的機器碼,而不是使用解釋器。它通過實現像很多現代JavaScript引擎(比如SpiderMonkey或Rhino)所用的JIT(即時)編譯器,從而將JavaScript代碼編譯成機器碼。這里主要區別在于V8不會產生字節碼或任何中間代碼。
V8曾經有兩個編譯器
在V8 的5.9版(今年早些時候發布)出現之前,V8引擎用了兩個編譯器:
full-codegen - 一個簡單而超快的編譯器,可以生成簡單而相對較慢的機器碼。
Crankshaft - 一個更復雜(即時)的優化的編譯器,可以生成高度優化的代碼。
V8引擎還在內部使用多個線程:
主線程執行我們想讓它干的活:獲取代碼,編譯然后執行它
還有一個單獨的線程用于編譯,這樣在主線程繼續執行的同時,單獨的線程能同時在優化代碼
一個Profiler線程,用于讓運行時知道哪些方法花了大量時間,這樣Crankshaft就可以對它們進行優化
幾個線程用于處理垃圾收集器清掃
第一次執行JavaScript代碼時,V8會利用full-codegen直接將解析的JavaScript翻譯為機器碼,而無需任何轉換。這就讓它能非常快地開始執行機器碼。請注意,由于V8不會使用中間字節碼表示,這樣就無需解釋器。
代碼運行了一段時間后,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就會創建一個基于C0的第二個隱藏類C1。C1描述了內存中的位置(相對于對象指針),屬性x在這個位置可以找到。此時,x存儲在偏移地址0處,就是說,當將內存中的point對象作為連續緩沖器來查看時,第一個偏移地址就對應于屬性x。V8也會用“類轉換”來更新C0,指出如果將一個屬性x添加到點對象,那么隱藏類應該從C0切換到C1。下面的point對象的隱藏類現在是C1。
每當向對象添加一個新屬性時,舊的隱藏類就被用一個轉換路徑更新為新的隱藏類。隱藏類轉換很重要,因為它們可以讓隱藏類在以相同方式創建的對象之間共享。如果兩個對象共享一個隱藏類,并且將相同的屬性添加到這兩個對象中,那么轉換會確保兩個對象都接收到相同的新隱藏類和它附帶的所有優化過的代碼。
當執行語句this.y = y(同樣是在Point函數內部,this.x = x語句之后)時,會重復此過程。
這時,又創建一個名為C2的新隱藏類,類轉換被添加到C1,表示如果將屬性y添加到Point對象(已包含屬性x),那么隱藏類應更改為C2,同時point對象的隱藏類被更新為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, 4); p2.b = 7; p2.a = 8;
現在,你可能會認為p1和p2會使用相同的隱藏類和轉換。嗯,這是錯的。對于p1,首先是添加屬性a,然后是屬性b。不過,對于p2,先是給b賦值,然后才是a。因此,由于轉換路徑不同,p1和p2最終會有不同的隱藏類。在這種情況下,以相同的順序初始化動態屬性要更好,這樣隱藏類才可以被重用。
內聯緩存
V8利用另一種稱為內聯緩存(inline caching)的技術來優化動態類型語言。內聯緩存來自于觀察的結果:對同一方法的重復調用往往發生在同一類型的對象上。關于內聯緩存的深入解釋可以在這里找到。
下面我們打算談談內聯緩存的一般概念(如果您沒有時間閱讀上面的深入解釋的話)。
那么它是如何工作的呢?V8維護在最近的方法調用中作為參數傳遞的對象類型的緩存,并使用該信息對將來作為參數傳遞的對象類型做出假設。如果V8能夠對傳遞給方法的對象類型做出一個很好的假設,那么它可以繞過算出如何訪問對象的屬性的過程,轉而使用先前查找對象的隱藏類時所存儲的信息。
那么隱藏類和內聯緩存的概念是如何關聯的呢?無論何時在特定對象上調用方法,V8引擎必須對該對象的隱藏類執行查找,以確定訪問特定屬性的偏移地址。在對同一個隱藏類的同一方法進行了兩次成功的調用之后,V8就省掉了隱藏類查找,只將屬性的偏移地址添加到對象指針本身上。對于所有將來對該方法的調用,V8引擎都會假定隱藏類沒有改變,并使用先前查找中存儲的偏移地址直接跳轉到特定屬性的內存地址。這會大大提高執行速度。
內聯緩存也是為什么同一類型的對象共享隱藏類非常重要的原因。如果您創建相同類型的兩個對象,但是用的是不同的隱藏類(如前面的示例),那么V8將無法使用內聯緩存,因為即使兩個對象的類型相同,但是它們的對應隱藏類也會為其屬性分配不同的偏移地址。
兩個對象基本相同,但是“a”和“b”屬性是按照不同的順序創建的。
編譯到機器碼
一旦Hydrogen圖被優化,Crankshaft將其降低到一個稱為Lithium的較低級別表示。大多數Lithium實現都是針對架構的。寄存器分配發生在這一級。
最后,Lithium被編譯成機器碼。然后其他事情,也就是OSR(當前棧替換,on-stack replacement),發生了。在我們開始編譯和優化一個明顯要長期運行的方法之前,我們可能會運行它。V8不會蠢到忘記它剛剛慢慢執行的代碼,所以它不會再用優化版本又執行一遍,而是將轉換所有已有的上下文(棧、寄存器),以便我們可以在執行過程中間就切換到優化版本。這是一個非常復雜的任務,請記住,除了其他優化之外,V8最開始時已經內聯了代碼。V8并非唯一能夠做到這一點的引擎。
有一種稱為去優化的保護措施,會作出相反的轉換,并恢復為非優化代碼,以防引擎的假設不再成立。
垃圾回收
對于垃圾回收來說,V8采用的是標記、清掃這種傳統分代方式來清除舊一代。標記階段應該停止執行JavaScript。為了控制GC成本,并使執行更加穩定,V8使用增量式標記:不是遍歷整個堆,嘗試標記每一個可能的對象,而是只遍歷一部分堆,然后恢復正常執行。下一個GC停止會從之前的堆遍歷停止的地方繼續。這就允許在正常執行期間有非常短的暫停。如前所述,清掃階段是由單獨的線程處理。
Ignition 和 TurboFan
隨著2017年早些時候版本5.9的發布,V8引入了一個新的執行管道。這個新的管道在真實的JavaScript應用程序中實現了更大的性能提升和顯著的內存節省。
這個新的執行管道建立在V8的解釋器Ignition 和V8的最新優化編譯器TurboFan之上。
您可以在這里查看V8團隊關于這個主題的博文。
自從5.9版本發布以來,V8不再用full-codeget 和 Crankshaft(自2010年以來V8所用的技術)執行JavaScript,因為V8團隊一直在努力跟上新的JavaScript語言特性,而這些特性需要優化。
這意味著V8整體下一步會有更簡單和更易維護的架構。
在Web和Node.js基準測試上的提升
這些提升僅僅是開始。新的Ignition和TurboFan管道為進一步優化鋪平了道路,這將在未來幾年內促進JavaScript性能提升,并縮小V8在Chrome和Node.js中所占比重。
最后,這里有一些關于如何編寫良好優化、更佳的JavaScript的訣竅。當然,從上面的內容不難得到這些訣竅,不過,為了方便起見,這里還是給出一個摘要:
如何編寫優化的JavaScript
對象屬性的順序:始終以相同的順序實例化對象屬性,以便可以共享隱藏類和隨后優化的代碼。
動態屬性:在實例化后向對象添加屬性會強制修改隱藏類,減慢為之前的隱藏類優化了的方法。所以應該在構造函數中指定對象的所有屬性。
方法:重復執行相同方法的代碼將比只執行一次的代碼(由于內聯緩存)運行得快。
數組:避免鍵不是增量數字的稀疏數組。元素不全的稀疏數組是一個哈希表,而訪問這種數組中的元素更昂貴。另外,盡量避免預分配大數組。最好隨著發展而增長。最后,不要刪除數組中的元素。它會讓鍵變得稀疏。
標記值:V8用32位表示對象和數字。它用一位來判斷是對象(flag = 1)還是整數(flag=0)(這個整數稱為SMI(SMall Integer,小整數),因為它是31位)。然后,如果一個數值大于31位,V8將會對數字裝箱,將其轉化為 double,并創建一個新對象將該數字放在里面。所以要盡可能使用31位有符號數字,從而避免昂貴的轉換為JS對象的裝箱操作。
我們在SessionStack中試圖在編寫高度優化的JavaScript代碼中遵循這些最佳實踐。原因是一旦將SessionStack集成到產品web應用程序中,它就開始記錄所有內容:所有DOM更改、用戶交互、JavaScript異常、棧跟蹤、失敗的網絡請求和調試消息。用SessionStack,您可以將Web應用中的問題重放為視頻,并查看用戶發生的一切。而所有這些都是在對您的web應用程序的性能不會產生影響的情況下發生的。
好了同學們,我能介紹的也都全部介紹完給你們了,如果下獲得更多JAVA教學資源,可以選擇來我們這里共同交流,群:24044837,很多大神在這里切磋學習,不懂可以直接問,晚上還有大牛免費直播教學。
注:加群要求
1、具有一定工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加,有些應屆生和實習生也可以加。
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。
3、如果沒有工作經驗,但基礎非常扎實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。
5.阿里Java高級大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!
PS:現在主要講解的內容是(反射原理、枚舉原理與應用、注解原理、常用設計模式、正規表達式高級應用、JAVA操作Office原理詳解、JAVA圖像處理技術,等多個知識點的詳解和實戰)
6.小號或者小白之類加群一律不給過,謝謝。
最后,每一位讀到這里的網友,感謝你們能耐心地看完。覺得對你有幫助可以給個喜歡!希望在成為一名更優秀的Java程序員的道路上,我們可以一起學習、一起進步
資源
https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub
https://github.com/thlorenz/v8-perf
http://code.google.com/p/v8/wiki/UsingGit
http://mrale.ph/v8/resources.html