主要包含三部分:堆空間、棧空間、代碼空間。javascript
先看一段代碼:java
function foo() {
var a = "極客時間";
var b = a;
var c = { name: "極客時間" };
var d = c;
}
foo();
複製代碼
普通類型
的變量都被保存在執行上下文中
,執行上下文又被壓入到棧中。而引用類型
的變量則是在棧中存放訪問堆空間的地址,其值被保存在堆空間中
。JS 中的垃圾數據分爲:棧中的垃圾數據與堆中的垃圾數據算法
如下面這段代碼爲例分析:瀏覽器
function foo() {
var a = 1;
var b = { name: "極客邦" };
function showName() {
var c = "極客時間";
var d = { name: "極客時間" };
}
showName();
}
foo();
複製代碼
經過 ESP 指針(即記錄當前執行狀態的指針)來操做,ESP 指向 showName 的函數上下文時,表示當前正在執行 showName 函數。性能優化
當 showName 執行完畢後,ESP 下移指向 foo 的執行上下文,這個操做就是銷燬 showName 的函數執行上下文。網絡
下圖爲從棧中回收 showName 執行上下文的過程
架構
當 foo 執行結束後,ESP 就指向全局上下文了,此時內存狀態以下圖。
函數
在 V8 中把堆分爲新生代
和老生代
兩個區域,新生代存放生存時間很短的對象,容量只有 1~8M;老生代中存放生存時間久的對象,容量很大。性能
對應的就有兩種垃圾回收器,副垃圾回收器與主垃圾回收器。 主垃圾回收器:主要負責老生代的垃圾回收。 副垃圾回收器:主要負責新生代的垃圾回收。大數據
兩種回收器有一套共同的執行流程。
主要用於回收新生代的垃圾,故內存不大。
下圖爲 V8 中的堆空間分佈。
新生代經過 Scavenge 算法將空間劃分爲對象區域與空閒區域。 每當對象區域被寫滿時,就會執行一次垃圾回收,具體過程以下:
因爲複製操做須要時間成本且操做頻繁,因此爲了執行效率,新生代的空間都不會太大;若通過兩次 GC 回收依然存活,就會將活着的對象移到老生代中,這就是 JS 引擎的對象晉升策略
。
主要用於回收老生代的垃圾,除了新生代中晉升的對象,一些大的對象會被直接分配到老生代。
老生代對象的兩個特色:
基於上述兩個特色,主垃圾回收器採用標記-清除(Mark-Sweep)
的算法進行垃圾回收。
標記:從一組根元素開始,遞歸遍歷這組根元素,能到達的元素成爲活動對象
,沒有到達的爲垃圾數據
。
function foo() {
var a = 1;
var b = { name: "極客邦" };
function showName() {
var c = "極客時間";
var d = { name: "極客時間" };
}
showName();
}
foo();
複製代碼
仍是這段代碼,當 showName 函數退出後,調用棧和堆空間以下圖:
下圖爲垃圾清除的過程
對一塊內存屢次執行標記-清除算法後,會產生大量不連續的內存碎片。因而又出現了另外一種算法標記-整理(Mark-Compact)
,標記過程都是同樣的,標記完後將全部的活動對象都向一端移動,而後直接清除掉端邊界之外的內存。
全停頓:JS 是運行在主線程之上的,一旦執行垃圾回收,就須要將正在執行的 JS 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行,這期間應用的性能和響應能力都會直線降低。這個過程就叫作全停頓(Stop-The-World)。
V8 新生代垃圾回收由於空間小,存活對象少,全停頓影響不大;但老生代就不同了,好比正在執行 JS 動畫,GC 工做致使主線程不能作其餘事情,那個動畫在這段時間沒法執行,頁面就會卡頓。
爲下降卡頓,因而誕生了增量標記(Incremental Marking)算法
。 增量標記:V8 將標記過程分爲一個個子標記過程,同時讓垃圾回收器和 JS 應用邏輯交替進行,直至標記完成。這些小任務執行時間短,穿插再 JS 任務間執行,這樣就不會感覺到頁面卡頓了。
編譯型語言
在程序執行前,需通過編譯器編譯,而且編譯以後會直接保留機器能讀懂的二進制文件;之後每次運行程序時,直接運行二進制文件,不須要從新編譯。
解釋型語言 在每次運行時都要通過解釋器對程序進行動態解釋和執行。
將源代碼轉換爲編譯器和解釋器能夠理解的 AST,並生成執行上下文。
爲何要生成 AST 以及什麼是 AST?
先說第一個,由於編譯器和解釋器沒法理解高級語言,因此要生成 AST,就像渲染引擎將 HTML 轉換爲計算機能夠理解的 DOM 樹同樣。
再來講第二個,AST 是代碼的結構化表示,有着很是重要的做用。
Babel 原理: 將 ES6 源碼轉換爲 AST,再將 ES6 語法的 AST 轉換爲 ES5 語法的 AST,最後利用它來生成 ES5 源代碼。
ESLint 原理:檢測流程也是將源碼轉換爲 AST,再利用 AST 來檢查代碼規範。
如何生成 AST?
兩個階段:分詞(詞法分析),解析(語法分析)。
詞法分析:將源碼拆解成最小的、不可再分的 token(關鍵字、標識符、賦值、字符串)。
語法分析:根據規則將上一步的 token 轉換爲 AST。若源碼存在語法錯誤,則不會生成 AST。
有了 AST 之後,V8 就會生成這段代碼的執行上下文。
有了 AST 和執行上下文後,解釋器根據 AST 生成字節碼並解釋執行。
爲何不直接轉成機器碼,而是經過字節碼轉成機器碼? 看下圖:
由上圖能夠看出,機器碼佔用的空間遠大於字節碼,早期手機內存只有 512M,而 V8 須要消耗大量內存來存放轉換後的機器碼,爲了解決內存佔用問題,就有了如今這套架構。
字節碼:介於 AST 與機器碼之間的一種代碼,須要經過解釋器將其轉爲機器碼後才能執行。
第一次執行字節碼時,解釋器會逐條解釋執行,若是發現有熱點代碼
(即一段代碼被重複執行屢次),編譯器就會將這段熱點代碼編譯爲機器碼保存起來,當再次執行這段代碼時,只須要執行編譯後的機器碼;這種技術就叫作即時編譯(JIT)
。
將優化的中心聚焦在單次腳本執行時間和腳本的網絡下載上。