web前端學習知識點:V8的垃圾回收和記憶體限制

千鋒HTML5學院發表於2019-12-02

前言

在第三次瀏覽器大戰中,來自Google的Chrome瀏覽器憑藉優異的效能成為聚光燈下的焦點。而Chrome的成功離不開站在其背後的JavaScript引擎V8。

隨著V8的出現,讓JavaScript徹底擺脫了作為指令碼語言效能低下的形象。V8出色的效能讓JavaScript出現在高效能後臺服務程式開發的舞臺上。也正是因為這樣的契機,在2009年,Node的創始人Ryan Dahl選擇了V8作為Node的JavaScript指令碼引擎。在事件驅動、非阻塞I/O模型的設計下實現了Node。

但是需要了解的是,Node雖然在JavaScript的執行上受益於V8,極大的擴寬了JavaScript的應用場景,讓其應用場景從客戶端進軍到了服務端,但是也同時受到了v8的限制,對於效能敏感的服務端的程式,記憶體管理、垃圾回收都會對服務的構成產生影響,而這些都和v8有著很大的關係。

image/20191202/dcf8fc23d74266757c504c78d13d7ad7.jpeg

V8的記憶體限制

在Node中如果透過JavaScript使用記憶體操作時會發現實際只能使用部分記憶體(64位系統下約為1.4G,32位系統下約為0.7G),這種限制對於其他的服務端開發語言來說基本上都是不存在的。

而V8的這種限制導致的結果是Node無法直接操作大記憶體物件。在單個Node程式的情況下,計算機的記憶體資源無法得到充足的使用。

而問題的原因在於Node是基於V8構建,所以在Node中使用物件都是透過V8自己的方式進行分配和管理。

而其記憶體管理機制在瀏覽器的場景下問題不大,但是對於Node,卻使得Node有了這般限制。

V8的垃圾回收演算法

V8的垃圾回收策略主要基於分代式垃圾回收機制。

在應用中,物件的生存週期長短不一,不同的演算法只能針對特定情況產生較為良好的效果。所以在現代的垃圾回收演算法中按物件的存活時間將記憶體的垃圾回收進行不同的分代,然後對不同分代的記憶體施以更高效的演算法。

在V8中,所有的JavaScript物件都是透過堆來進行分配。我們可以透過process.memoryUsage()這個方法返回的heapTotal和heapUsed來檢視堆的使用情況,前者是以申請的堆記憶體,後者是當前使用的量。如果在程式碼中使用的物件使用的空間超過了申請的空間,那麼就會繼續申請堆記憶體,直到堆的大小超過V8的限制。

在V8中,主要將堆分為新生代和老生代兩個區域,新生代中存放的是生存時間短的物件,老生代中存放的生存時間久的物件。

新生區通常只支援 1~8M 的容量,而老生區支援的容量就大很多了。對於這兩塊區域,V8 分別使用兩個不同的垃圾回收器,以便更高效地實施垃圾回收。

· 副垃圾回收器,主要負責新生代的垃圾回收。

· 主垃圾回收器,主要負責老生代的垃圾回收。

不論什麼型別的垃圾回收器,它們都有一套共同的執行流程。

1. 第一步是標記空間中活動物件和非活動物件。所謂活動物件就是還在使用的物件,非活動物件就是可以進行垃圾回收的物件。

2. 第二步是回收非活動物件所佔據的記憶體。其實就是在所有的標記完成之後,統一清理記憶體中所有被標記為可回收的物件。

3. 第三步是做記憶體整理。一般來說,頻繁回收物件後,記憶體中就會存在大量不連續空間,我們把這些不連續的記憶體空間稱為記憶體碎片,。當記憶體中出現了大量的記憶體碎片之後,如果需要分配較大連續記憶體的時候,就有可能出現記憶體不足的情況。所以最後一步需要整理這些記憶體碎片。(這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片).

新生代中的垃圾回收

新生代中用Scavenge 演算法來處理,把新生代空間對半劃分為兩個區域,一半是物件區域,一半是空閒區域。新加入的物件都會存放到物件區域,當物件區域快被寫滿時,就需要執行一次垃圾清理操作。

在垃圾回收過程中,首先要對物件區域中的垃圾做標記;標記完成之後,就進入垃圾清理階段,副垃圾回收器會把這些存活的物件複製到空閒區域中,同時它還會把這些物件有序地排列起來,所以這個複製過程,也就相當於完成了記憶體整理操作,複製後空閒區域就沒有記憶體碎片了。

完成複製後,物件區域與空閒區域進行角色翻轉,也就是原來的物件區域變成空閒區域,原來的空閒區域變成了物件區域。這樣就完成了垃圾物件的回收操作,同時這種角色翻轉的操作還能讓新生代中的這兩塊區域無限重複使用下去.

為了執行效率,一般新生區的空間會被設定得比較小,也正是因為新生區的空間不大,所以很容易被存活的物件裝滿整個區域。為了解決這個問題,JavaScript 引擎採用了物件晉升策略,也就是經過兩次垃圾回收依然還存活的物件,會被移動到老生區中。

老生代中的垃圾回收

老生代中用標記 - 清除(Mark-Sweep)的演算法來處理。首先是標記過程階段,標記階段就是從一組根元素開始,遞迴遍歷這組根元素(遍歷呼叫棧),在這個遍歷過程中,能到達的元素稱為活動物件,沒有到達的元素就可以判斷為垃圾資料.然後在遍歷過程中標記,標記完成後就進行清除過程。它和副垃圾回收器的垃圾清除過程完全不同,這個的清除過程是刪除標記資料。

清除演算法後,會產生大量不連續的記憶體碎片。而碎片過多會導致大物件無法分配到足夠的連續記憶體,於是又產生了標記 - 整理(Mark-Compact)演算法,這個標記過程仍然與標記 - 清除演算法裡的是一樣的,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,從而讓存活物件佔用連續的記憶體塊。

增量標記演算法和全停頓

由於 JavaScript 是執行在主執行緒之上的,一旦執行垃圾回收演算法,都需要將正在執行的 JavaScript 指令碼暫停下來,待垃圾回收完畢後再恢復指令碼執行。我們把這種行為叫做全停頓。

在 V8 新生代的垃圾回收中,因其空間較小,且存活物件較少,所以全停頓的影響不大,但老生代就不一樣了。如果執行垃圾回收的過程中,佔用主執行緒時間過久,主執行緒是不能做其他事情的。比如頁面正在執行一個 JavaScript 動畫,因為垃圾回收器在工作,就會導致這個動畫在垃圾回收過程中無法執行,這將會造成頁面的卡頓現象。

為了降低老生代的垃圾回收而造成的卡頓,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個演算法稱為增量標記(Incremental Marking)演算法.

使用增量標記演算法,可以把一個完整的垃圾回收任務拆分為很多小的任務,這些小的任務執行時間比較短,可以穿插在其他的 JavaScript 任務中間執行,這樣當執行上述動畫效果時,就不會讓使用者因為垃圾回收任務而感受到頁面的卡頓了。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69917019/viewspace-2666576/,如需轉載,請註明出處,否則將追究法律責任。

相關文章