一、瀏覽器核心-渲染引擎
渲染就是根據描述或者定義構建一個資料模型,生成圖形的過程。
瀏覽器核心就是將頁面(Html、Css、JavaScript)構建成視覺化、可聽化的多媒體結果。
我們也可以將瀏覽器核心稱之為"渲染引擎",渲染引擎經常做的事情就是將Html、Css、JavaScript文字或者其他的資原始檔轉換成我們瀏覽器網頁。
二、JavaScript引擎
2.1JavaScript引擎(JavaScript Engine)
當我們在執行一段程式碼時,真正賦予這段程式碼生命的就是JavaScript引擎(JavaScript Engine)。JavaScript引擎是一個專門處理JavaScript指令碼的虛擬機器,一般會附帶在網頁瀏覽器之中。
JavaScript引擎有許多種:
- V8 — 開源,由 Google 開發,用 C ++ 編寫。
- Rhino — 由 Mozilla 基金會管理,開源,完全用 Java 開發。
- SpiderMonkey — 是第一個支援 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用。
- JavaScriptCore — 開源,以Nitro形式銷售,由蘋果為Safari開發。
- KJS — KDE 的引擎,最初由 Harri Porten 為 KDE 專案中的 Konqueror 網頁瀏覽器開發。
- Chakra (JScript9) — Internet Explorer。
- Chakra (JavaScript) — Microsoft Edge。
- Nashorn, 作為 OpenJDK 的一部分,由 Oracle Java 語言和工具組編寫。
- JerryScript — 物聯網的輕量級引擎。
而最為大家熟知的無疑是V8引擎,他用於Chrome瀏覽器和Node中。
JavaScript引擎從頭到尾負責整個JavaScript程式的編譯和執行過程。
2.2渲染引擎和JavaScript引擎的關係
- 渲染引擎通過呼叫介面來處理JavaScript的邏輯。
- JavaScript引擎通過橋接介面來訪問渲染引擎的DOM、CSSDOM等。
2.3JavaScript引擎如何工作?
JavaScript引擎主要組成部分:
- 編譯器:負責語法分析和程式碼生成。
- 解析器:負責接收字碼節和解析執行字碼節。
- JIT工具:將字碼節或者抽象語法樹轉換為原生程式碼(可執行程式碼)。
- 垃圾回收器和分析工具(Profiler):負責垃圾回收和收集引擎中的資訊,幫助改善引擎的效能和功效。
JavaScript本質上是一種解釋型語言,與編譯型語言不同的是它需要一遍執行一邊解析,而編譯型語言在執行時已經完成編譯,可直接執行,有更快的執行速度。
三、JavaScript-V8引擎的編譯和執行
3.1資料表示
- 基本資料型別:Boolean、Number、String、Null、Undefined、Symbol、BigInt(起草第二階段,但是以後在JavaScript中使用時板上釘釘的事情了)
- 物件型別(Array、Object、Date、Error等等)。 在V8中,資料表示分為兩部分:
- 資料的實際內容,它們是變長的,而且內容的型別也不一樣。
- 資料的控制程式碼,大小是固定的,包含指向第一部分資料的指標。
3.2控制程式碼Handle
V8需要進行垃圾回收,並需要移動這些資料內容,如果直接使用指標的話就會出問題或者需要比較大的開銷。使用控制程式碼就不存在這些問題,只需要修改控制程式碼中的指標即可,使用者使用的還是控制程式碼,它本身沒有發生變化。
3.3記憶體堆和呼叫棧
- mory Heap(記憶體堆) — 記憶體分配地址的地方
- Call Stack(呼叫棧) — 程式碼執行的地方
3.3編譯執行過程
四、V8記憶體分配
4.1小記憶體區塊Zone類
管理一系列的小塊記憶體,這些小記憶體的生命週期類似,可以使用一個Zone物件。 Zone物件先對自己申請一塊記憶體,然後管理和分配一些小記憶體。當一塊小記憶體被分配之後,不能被Zone回收,只能一次性回收Zone分配的所有小記憶體。例如:抽象語法樹的記憶體分配和使用,在構建之後,會生成原生程式碼,然後其記憶體被一次性全部收回,效率非常高。
但是有一個嚴重的缺陷,當一個過程需要很多記憶體,Zone將需要分配大量的記憶體,卻又不能及時回收,會導致記憶體不足情況。
4.2堆記憶體
V8使用堆來管理JavaScript使用的資料、以及生成的程式碼、雜湊表等。為了更方便地實現垃圾回收,同很多虛擬機器一樣,V8將堆分成三個部分。年輕代、年老代、和大物件。
五、V8的垃圾回收機制
5.1新生代Scavenge(清除)演算法
主要採用Cheney(人名)演算法。一種採用複製的方式實現的垃圾回收演算法。
1、將新生代堆記憶體分一為二,每一部分空間稱為semispace。其中一個處於使用之中的稱為from空間,另一個處於閒置稱為to空間。
2、當我們分配物件時,先是在From空間中進行分配。
3、垃圾回收時,檢查from空間內的存活物件,一是否經歷過清除回收,二to空間是否已經使用了25%(保證新分配有足夠的空間)。
4、將這些存活物件複製到to空間中。非存活物件佔用的空間將會被釋放。
5、完成複製後,from空間與to空間角色發生對換。
注:實際使用的堆記憶體是新生代中的兩個semispace空間大小,和老生代所用記憶體大小之和。
如何判斷物件是否存活呢?作用域?是一套儲存和查詢變數的規則。這套規則決定了記憶體裡物件能否訪問。
特點:清除演算法是典型的犧牲空間換取時間的演算法,無法大規模地應用到所有回收中,卻非常適合應用在新生代生命週期短的變數。
5.2Mark-Sweep老生代標記清除
1、 標記階段遍歷堆中的所有物件,並標記活著的物件
2、 清除階段,只清除沒有被標記的物件。
最大的問題是,在進行一次標記清除之後會出現不連續的狀態。這種記憶體碎片會對後續的記憶體分配造成問題。很可能需要分配一個大物件時,所有的碎片空間都無法完成,就會提前觸發垃圾回收,而這次全量回收是不必要的。
5.3Mark-Compact老生代標記整理
在標記清除的基礎上發展而來,在整理的過程中
1、 將活著的物件往一段移動
2、 移動完成後,直接清理掉邊界外的記憶體
5.4Incremental Marking增量標記
垃圾回收的過程都需要將應用邏輯暫停下來。為了降低全量回收帶來的停頓時間,在標記階段,將原本一口氣要完成的動作改為增量標記。垃圾回收與應用邏輯交替執行到標記階段完成。最大停頓時間較少的1/6左右.
後續還引入了延遲清理與增量整理,讓清理和整理動作也變成增量式的。
六、Javascript的V8引擎為什麼快
- 針對上下文的Snapshot技術
什麼是上下文(Contexts)?實際是JS應用程式的執行環境,避免應用程式的修改相互影響,例如一個頁面js修改內建物件方法toString,不應該影響到另外頁面。chrome瀏覽器每個process只有一個V8引擎例項,瀏覽器中的每個視窗、iframe都對應一個上下文。
- Built-in的js程式碼
利用JS自表達內建物件、方法,如上面程式碼實現Math.min方法,從而V8在實現程式碼轉譯時只需注重基本操作,以%符號開頭的函式來自V8執行時函式。
- 建立AST(Abstract SyntaxTree)時記憶體的管理。
V8在建立AST後,對其進行彙編生成動態機器語言,所以AST在code generated後需要回收;針對AST建立過程中多結點記憶體申請和一次性回收的特點,V8使用了記憶體段連結串列管理,並結合scopelock模式,實現少數申請(Segment,8KB~1MB)、多次分配AST結點、一次回收各個Segment的管理方式,既能避免記憶體碎片,又可以避免遍歷AST結點逐個回收記憶體
- CompileCache避免相同程式碼重複編譯
對於一段JS程式碼,在開始進行詞法分析前,會從編譯快取區CompilationCache查詢該段程式碼是否已經被編譯過,如果是,則直接取出編譯過的機器程式碼,並返回,這樣降低CPU的使用率,換來記憶體空間一定的佔用;如果一個頁面中重複載入JS檔案,這方法的提速是很明顯的;這種做法應該有平衡對比過。
- 屬性的快速訪問
V8沒有像其它JS Engine使用詞典結構或紅黑樹實現的map來管理屬性,而是在每個物件附加一個指標,指向hidden class(如果第一次建立該型別物件,則新建hidden class);當物件每新增一個屬性時,將新建一個class(記錄了每個屬性的位移/位置),而原來的class指向新class,即建立起一個hidden class的轉換連結串列。
- Heap堆記憶體管理
- Inline caching減少函式呼叫開銷
函式繫結發生在執行時,所以無法通過method tables定位函式入口;通過該技術可以記錄函式入口,避免重複查詢。
- 一次性編譯生成機器語言
一般JS engine會在AST生成後,將之編譯為中間語言(bytecode),在執行時候再解析這些bytecode;Java 也同樣編譯為這些bytecode,再採用VM(實現跨平臺)作為直譯器,為了提高效能,Java採用混雜方式,把無關平臺、常用的程式碼編譯為機器程式碼。V8則是一次性把AST編譯為機器語言。從assembler相關檔案頭的Copyright可以看出,這些不同平臺(ia32, arm)下的編譯器,原型來自Sun Microsystems。