淺談Chrome V8引擎中的垃圾回收機制

垃圾回收器

JavaScript的垃圾回收器

JavaScript使用垃圾回收機制來自動管理內存。垃圾回收是一把雙刃劍,其好處是能夠大幅簡化程序的內存管理代碼,下降程序員的負擔,減小因 長時間運轉而帶來的內存泄露問題。但使用了垃圾回收即意味着程序員將沒法掌控內存。ECMAScript沒有暴露任何垃圾回收器的接口。咱們沒法強迫其進 行垃圾回收,更沒法干預內存管理程序員

內存管理問題

在瀏覽器中,Chrome V8引擎實例的生命週期不會很長(誰沒事一個頁面開着幾天幾個月不關),並且運行在用戶的機器上。若是不幸發生內存泄露等問題,僅僅會 影響到一個終端用戶。且不管這個V8實例佔用了多少內存,最終在關閉頁面時內存都會被釋放,幾乎沒有太多管理的必要(固然並不表明一些大型Web應用不需 要管理內存)。但若是使用Node做爲服務器,就須要關注內存問題了,一旦內存發生泄漏,長此以往整個服務將會癱瘓(服務器不會頻繁的重啓)算法

Chrome的內存限制

存在限制

Chrome限制了所能使用的內存極限(64位爲1.4GB,32位爲1.0GB),這也就意味着將沒法直接操做一些大內存對象。sql

爲什麼限制

Chrome之因此限制了內存的大小,表面上的緣由是V8最初是做爲瀏覽器的JavaScript引擎而設計,不太可能遇到大量內存的場景,而深層次的緣由 則是因爲V8的垃圾回收機制的限制。因爲V8須要保證JavaScript應用邏輯與垃圾回收器所看到的不同,V8在執行垃圾回收時會阻塞 JavaScript應用邏輯,直到垃圾回收結束再從新執行JavaScript應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。 若V8的堆內存爲1.5GB,V8作一次小的垃圾回收須要50ms以上,作一次非增量式的垃圾回收甚至要1秒以上。這樣瀏覽器將在1s內失去對用戶的響 應,形成假死現象。若是有動畫效果的話,動畫的展示也將顯著受到影響瀏覽器

 

Chrome V8的堆構成

V8的堆其實並不僅是由老生代和新生代兩部分構成,能夠將堆分爲幾個不一樣的區域:
* 新生代內存區:大多數的對象被分配在這裏,這個區域很小可是垃圾回特別頻繁
* 老生代指針區:屬於老生代,這裏包含了大多數可能存在指向其餘對象的指針的對象,大多數重新生代晉升的對象會被移動到這裏
* 老生代數據區:屬於老生代,這裏只保存原始數據對象,這些對象沒有指向其餘對象的指針
* 大對象區:這裏存放體積超越其餘區大小的對象,每一個對象有本身的內存,垃圾回收其不會移動大對象
* 代碼區:代碼對象,也就是包含JIT以後指令的對象,會被分配在這裏。惟一擁有執行權限的內存區
* Cell區、屬性Cell區、Map區:存放Cell、屬性Cell和Map,每一個區域都是存放相同大小的元素,結構簡單服務器

每一個區域都是由一組內存頁構成,內存頁是V8申請內存的最小單位,除了大對象區的內存頁較大之外,其餘區的內存頁都是1MB大小,並且按照1MB對 齊。內存頁除了存儲的對象,還有一個包含元數據和標識信息的頁頭,以及一個用於標記哪些對象是活躍對象的位圖區。另外每一個內存頁還有一個單獨分配在另外內 存區的槽緩衝區,裏面放着一組對象,這些對象可能指向其餘存儲在該頁的對象。垃圾回收器只會針對新生代內存區、老生代指針區以及老生代數據區進行垃圾回收性能

Chrome V8的垃圾回收機制

如何判斷回收內容

如何肯定哪些內存須要回收,哪些內存不須要回收,這是垃圾回收期須要解決的最基本問題。咱們能夠這樣假定,一個對象爲活對象當且僅當它被一個根對象 或另外一個活對象指向。根對象永遠是活對象,它是被瀏覽器或V8所引用的對象。被局部變量所指向的對象也屬於根對象,由於它們所在的做用域對象被視爲根對 象。全局對象(Node中爲global,瀏覽器中爲window)天然是根對象。瀏覽器中的DOM元素也屬於根對象動畫

如何識別指針和數據

垃圾回收器須要面臨一個問題,它須要判斷哪些是數據,哪些是指針。因爲不少垃圾回收算法會將對象在內存中移動(緊湊,減小內存碎片),因此常常須要進行指針的改寫spa

目前主要有三種方法來識別指針:
1. 保守法:將全部堆上對齊的字都認爲是指針,那麼有些數據就會被誤認爲是指針。因而某些實際是數字的假指針,會背誤認爲指向活躍對象,致使內存泄露(假指針指向的對象多是死對象,但依舊有指針指向——這個假指針指向它)同時咱們不能移動任何內存區域。
2. 編譯器提示法:若是是靜態語言,編譯器可以告訴咱們每一個類當中指針的具體位置,而一旦咱們知道對象時哪一個類實例化獲得的,就能知道對象中全部指針。這是JVM實現垃圾回收的方式,但這種方式並不適合JS這樣的動態語言
3. 標記指針法:這種方法須要在每一個字末位預留一位來標記這個字段是指針仍是數據。這種方法須要編譯器支持,但實現簡單,並且性能不錯。V8採用的是這種方式。V8將全部數據以32bit字寬來存儲,其中最低一位保持爲0,而指針的最低兩位爲01設計

V8的回收策略

自動垃圾回收算法的演變過程當中出現了不少算法,可是因爲不一樣對象的生存週期不一樣,沒有一種算法適用於全部的狀況。因此V8採用了一種分代回收的策 略,將內存分爲兩個生代:新生代和老生代。新生代的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。分別對新生代和老生代使用 不一樣的垃圾回收算法來提高垃圾回收的效率。對象起初都會被分配到新生代,當新生代中的對象知足某些條件(後面會有介紹)時,會被移動到老生代(晉升)指針

V8的分代內存

默認狀況下,64位環境下的V8引擎的新生代內存大小32MB、老生代內存大小爲1400MB,而32位則減半,分別爲16MB和700MB。V8內存的最大保留空間分別爲1464MB(64位)和732MB(32位)。具體的計算公式是4*reserved_semispace_space_ + max_old_generation_size_,新生代由兩塊reserved_semispace_space_組成,每塊16MB(64位)或8MB(32位)

新生代

新生代的特色

大多數的對象被分配在這裏,這個區域很小可是垃圾回特別頻繁。在新生代分配內存很是容易,咱們只須要保存一個指向內存區的指針,不斷根據新對象的大小進行遞增便可。當該指針到達了新生代內存區的末尾,就會有一次清理(僅僅是清理新生代)

新生代的垃圾回收算法

新生代使用Scavenge算法進行回收。在Scavenge算法的實現中,主要採用了Cheney算法。

Cheney算法算法是一種採用複製的方式實現的垃圾回收算法。它將內存一分爲二,每一部分空間稱爲semispace。在這兩個 semispace中,一個處於使用狀態,另外一個處於閒置狀態。處於使用狀態的semispace空間稱爲From空間,處於閒置狀態的空間稱爲To空 間,當咱們分配對象時,先是在From空間中進行分配。當開始進行垃圾回收算法時,會檢查From空間中的存活對象,這些存活對象將會被複制到To空間中 (複製完成後會進行緊縮),而非活躍對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換。也就是說,在垃圾回收的過程當中,就是 經過將存活對象在兩個semispace之間進行復制。能夠很容易看出來,使用Cheney算法時,總有一半的內存是空的。可是因爲新生代很小,因此浪費 的內存空間並不大。並且因爲新生代中的對象絕大部分都是非活躍對象,須要複製的活躍對象比例很小,因此其時間效率十分理想。複製的過程採用的是BFS(廣 度優先遍歷)的思想,從根對象出發,廣度優先遍歷全部能到達的對象

具體的執行過程大體是這樣:

首先將From空間中全部能從根對象到達的對象複製到To區,而後維護兩個To區的指針scanPtr和allocationPtr,分別指向即將 掃描的活躍對象和即將爲新對象分配內存的地方,開始循環。循環的每一輪會查找當前scanPtr所指向的對象,肯定對象內部的每一個指針指向哪裏。若是指向 老生代咱們就沒必要考慮它了。若是指向From區,咱們就須要把這個所指向的對象從From區複製到To區,具體複製的位置就是allocationPtr 所指向的位置。複製完成後將scanPtr所指對象內的指針修改成新複製對象存放的地址,並移動allocationPtr。若是一個對象內部的全部指針 都被處理完,scanPtr就會向前移動,進入下一個循環。若scanPtr和allocationPtr相遇,則說明全部的對象都已被複制完,From 區剩下的均可以被視爲垃圾,能夠進行清理了

舉個栗子(以及湊篇幅),若是有相似以下的引用狀況:

+----- A對象 | 根對象----+----- B對象 ------ E對象 | +----- C對象 ----+---- F對象 | +---- G對象 ----- H對象 D對象 

在執行Scavenge以前,From區長這幅模樣

+---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ 

那麼首先將根對象能到達的ABC對象複製到To區,因而乎To區就變成了這個樣子:

allocationPtr
             ↓ 
+---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+ ↑ scanPtr 

接下來進入循環,掃描scanPtr所指的A對象,發現其沒有指針,因而乎scanPtr移動,變成以下這樣

allocationPtr
             ↓ 
+---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+ ↑ scanPtr 

接下來掃描B對象,發現其有指向E對象的指針,且E對象在From區,那麼咱們須要將E對象複製到allocationPtr所指的地方並移動allocationPtr指針:

allocationPtr
                 ↓ 
+---+---+---+---+------------------------+ | A | B | C | E | | +---+---+---+---+------------------------+ ↑ scanPtr 

B對象裏全部指針都已被複制完,因此移動scanPtr:

allocationPtr
                 ↓ 
+---+---+---+---+------------------------+ | A | B | C | E | | +---+---+---+---+------------------------+ ↑ scanPtr 

接下來掃描C對象,C對象中有兩個指針,分別指向F對象和G對象,且都在From區,先複製F對象到To區:

allocationPtr
                     ↓ 
+---+---+---+---+---+--------------------+ | A | B | C | E | F | | +---+---+---+---+---+--------------------+ ↑ scanPtr 

而後複製G對象到To區

allocationPtr
                         ↓ 
+---+---+---+---+---+---+----------------+ | A | B | C | E | F | G | | +---+---+---+---+---+---+----------------+ ↑ scanPtr 

這樣C對象內部的指針已經複製完成了,移動scanPtr:

allocationPtr
                         ↓ 
+---+---+---+---+---+---+----------------+ | A | B | C | E | F | G | | +---+---+---+---+---+---+----------------+ ↑ scanPtr 

逐個掃描E,F對象,發現其中都沒有指針,移動scanPtr:

allocationPtr
                         ↓ 
+---+---+---+---+---+---+----------------+ | A | B | C | E | F | G | | +---+---+---+---+---+---+----------------+ ↑ scanPtr 

掃描G對象,發現其中有一個指向H對象的指針,且H對象在From區,複製H對象到To區,並移動allocationPtr:

allocationPtr
                             ↓ 
+---+---+---+---+---+---+---+------------+ | A | B | C | E | F | G | H | | +---+---+---+---+---+---+---+------------+ ↑ scanPtr 

完成後因爲G對象沒有其餘指針,且H對象沒有指針移動scanPtr:

allocationPtr
                             ↓ 
+---+---+---+---+---+---+---+------------+ | A | B | C | E | F | G | H | | +---+---+---+---+---+---+---+------------+ ↑ scanPtr 

此時scanPtr和allocationPtr重合,說明覆制結束

能夠對比一下From區和To區在複製完成後的結果:

//From區
+---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ //To區 +---+---+---+---+---+---+---+------------+ | A | B | C | E | F | G | H | | +---+---+---+---+---+---+---+------------+ 

D對象沒有被複制,它將被做爲垃圾進行回收

寫屏障

若是新生代中的一個對象只有一個指向它的指針,而這個指針在老生代中,咱們如何判斷這個新生代的對象是否存活?爲了解決這個問題,須要創建一個列表用來記錄全部老生代對象指向新生代對象的狀況。每當有老生代對象指向新生代對象的時候,咱們就記錄下來

對象的晉升

當一個對象通過屢次新生代的清理依舊倖存,這說明它的生存週期較長,也就會被移動到老生代,這稱爲對象的晉升。具體移動的標準有兩種:
1. 對象從From空間複製到To空間時,會檢查它的內存地址來判斷這個對象是否已經經歷過一個新生代的清理,若是是,則複製到老生代中,不然複製到To空間中
2. 對象從From空間複製到To空間時,若是To空間已經被使用了超過25%,那麼這個對象直接被複制到老生代

老生代

老生代的特色

老生代所保存的對象大多數是生存週期很長的甚至是常駐內存的對象,並且老生代佔用的內存較多

老生代的垃圾回收算法

老生代佔用內存較多(64位爲1.4GB,32位爲700MB),若是使用Scavenge算法,浪費一半空間不說,複製如此大塊的內存消耗時間將 會至關長。因此Scavenge算法顯然不適合。V8在老生代中的垃圾回收策略採用Mark-Sweep和Mark-Compact相結合

Mark-Sweep(標記清除)

標記清除分爲標記和清除兩個階段。在標記階段須要遍歷堆中的全部對象,並標記那些活着的對象,而後進入清除階段。在清除階段總,只清除沒有被標記的對象。因爲標記清除只清除死亡對象,而死亡對象在老生代中佔用的比例很小,因此效率較高

標記清除有一個問題就是進行一次標記清楚後,內存空間每每是不連續的,會出現不少的內存碎片。若是後續須要分配一個須要內存空間較多的對象時,若是全部的內存碎片都不夠用,將會使得V8沒法完成此次分配,提早觸發垃圾回收。

Mark-Compact(標記整理)

標記整理正是爲了解決標記清除所帶來的內存碎片的問題。標記整理在標記清除的基礎進行修改,將其的清除階段變爲緊縮極端。在整理的過程當中,將活着的 對象向內存區的一段移動,移動完成後直接清理掉邊界外的內存。緊縮過程涉及對象的移動,因此效率並非太好,可是能保證不會生成內存碎片

算法思路

標記清除和標記整理都分爲兩個階段:標記階段、清除或緊縮階段

在標記階段,全部堆上的活躍對象都會被標記。每一個內存頁有一個用來標記對象的位圖,位圖中的每一位對應內存頁中的一個字。這個位圖須要佔據必定的空 間(32位下爲3.1%,64位爲1.6%)。另外有兩位用來標記對象的狀態,這個狀態一共有三種(因此要兩位)——白,灰,黑:
* 若是一個對象爲白對象,它還沒未被垃圾回收器發現
* 若是一個對象爲灰對象,它已經被垃圾回收器發現,但其鄰接對象還沒有所有處理
* 若是一個對象爲黑對象,說明他步進被垃圾回收器發現,其鄰接對象也所有被處理完畢了

若是將對中的對象看作由指針作邊的有向圖,標記算法的核心就是深度優先搜索。在初始時,位圖爲空,全部的對象也都是白對象。從根對象到達的對象會背 染色爲灰色,放入一個單獨的雙端隊列中。標記階段的每次循環,垃圾回收器都會從雙端隊列中取出一個對象並將其轉變爲黑對象,並將其鄰接的對象轉變爲灰,然 後把其鄰接對象放入雙端隊列。若是雙端隊列爲空或全部對象都變成黑對象,則結束。特別大的對象,可能會在處理時進行分片,防止雙端隊列溢出。若是雙端隊列 溢出,則對象仍然會成爲灰對象,但不會被放入隊列中,這將致使其鄰接對象沒法被轉變爲灰對象。因此在雙端隊列爲空時,須要掃描全部對象,若是仍有灰對象, 將它們從新放入隊列中進行處理。標記結束後,全部的對象都應該非黑即白,白對象將成爲垃圾,等待釋放

清除和緊縮階段都是之內存頁爲單位回收內存

清除時垃圾回收器會掃描連續存放的死對象,將其變成空閒空間,並保存到一個空閒空間的鏈表中。這個鏈表常被scavenge算法用於分配被晉升對象的內存,但也被緊縮算法用於移動對象

緊縮算法會嘗試將碎片頁整合到一塊兒來釋放內存。因爲頁上的對象會被移動到新的頁上,須要從新分配一些頁。大體過程是,對目標碎片頁中的每一個活躍對 象,在空閒內存鏈表中分配一塊內存頁,將該對象複製過去,並在碎片頁中的該對象上寫上新的內存地址。隨後在遷出過程當中,對象的舊地址將會被記錄下來,在遷 出結束後,V8會遍歷全部它所記錄的舊對象的地址,將其更新爲新地址。因爲標記過程當中也記錄了不一樣頁之間的指針,這些指針在此時也會進行更新。若是一個頁 很是活躍,如其中有過多須要記錄的指針,那麼地址記錄會跳過它,等到下一輪垃圾回收進行處理

結合使用標記清除和標記整理

Chrome V8的老生代使用標記清除和標記整理結合的方式,主要採用標記清除算法,若是空間不足以分配重新生代晉升過來的對象時,才使用標記整理

相關文章
相關標籤/搜索