Chrome V8系列--淺析Chrome V8引擎中的垃圾回收機制和內存泄露優化策略

V8 實現了準確式 GC,GC 算法採用了分代式垃圾回收機制。所以,V8 將內存(堆)分爲新生代和老生代兩部分。javascript

 

1、前言html

V8的垃圾回收機制:JavaScript使用垃圾回收機制來自動管理內存。垃圾回收是一把雙刃劍,其好處是能夠大幅簡化程序的內存管理代碼,下降程序員的負擔,減小因 長時間運轉而帶來的內存泄露問題。vue

但使用了垃圾回收即意味着程序員將沒法掌控內存。ECMAScript沒有暴露任何垃圾回收器的接口。咱們沒法強迫其進 行垃圾回收,更沒法干預內存管理java

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

 

2、chrome內存限制react

2.1存在限制git

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

2.2爲什麼限制github

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

 

3、chrome V8的堆構成

V8的堆其實並不僅是由老生代和新生代兩部分構成,能夠將堆分爲幾個不一樣的區域:

一、新生代內存區:大多數的對象被分配在這裏,這個區域很小可是垃圾回特別頻繁;

二、老生代指針區:屬於老生代,這裏包含了大多數可能存在指向其餘對象的指針的對象,大多數重新生代晉升的對象會被移動到這裏;

三、老生代數據區:屬於老生代,這裏只保存原始數據對象,這些對象沒有指向其餘對象的指針;

四、大對象區:這裏存放體積超越其餘區大小的對象,每一個對象有本身的內存,垃圾回收其不會移動大對象;

五、代碼區:代碼對象,也就是包含JIT以後指令的對象,會被分配在這裏。惟一擁有執行權限的內存區;

六、Cell區、屬性Cell區、Map區:存放Cell、屬性Cell和Map,每一個區域都是存放相同大小的元素,結構簡單。

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

 

4、chrome V8的垃圾回收機制

4.1如何判斷回收內容

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

 

4.2如何識別指針和數據

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

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

 

4.3 V8回收策略

自動垃圾回收算法的演變過程當中出現了不少算法,可是因爲不一樣對象的生存週期不一樣,沒有一種算法適用於全部的狀況。因此V8採用了一種分代回收的策 略,將內存分爲兩個生代:新生代和老生代

新生代的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。分別對新生代和老生代使用 不一樣的垃圾回收算法來提高垃圾回收的效率。對象起初都會被分配到新生代,當新生代中的對象知足某些條件(後面會有介紹)時,會被移動到老生代(晉升)。

 

5、新生代算法

新生代中的對象通常存活時間較短,使用 Scavenge GC 算法。在Scavenge的具體實現中,主要是採用一種複製的方式的方法--cheney算法。

在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,一定有一個空間是使用的,另外一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,若是有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

 

6、老生代算法

老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法標記壓縮算法

在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:

一、新生代中的對象是否已經經歷過一次 Scavenge 算法,若是經歷過的話,會將對象重新生代空間移到老生代空間中。

二、To 空間的對象佔比大小超過 25 %。在這種狀況下,爲了避免影響到內存分配,會將對象重新生代空間移到老生代空間中。

老生代中的空間很複雜,有以下幾個空間:

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不變的對象空間 NEW_SPACE, // 新生代用於 GC 複製算法的空間 OLD_SPACE, // 老生代常駐對象空間 CODE_SPACE, // 老生代代碼對象空間 MAP_SPACE, // 老生代 map 對象 LO_SPACE, // 老生代大空間對象 NEW_LO_SPACE, // 新生代大空間對象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };

在老生代中,如下狀況會先啓動標記清除算法:

一、某一個空間沒有分塊的時候

二、空間中被對象超過必定限制

三、空間不能保證新生代中的對象移動到老生代中

Mark Sweep 是將須要被回收的對象進行標記,在垃圾回收運行時直接釋放相應的地址空間,以下圖所示(紅色的內存區域表示須要被回收的區域):

Mark Compact 的思想有點像新生代垃圾回收時採起的 Cheney 算法:將存活的對象移動到一邊,將須要被回收的對象移動到另外一邊,而後對須要被回收的對象區域進行總體的垃圾回收。

在這個階段中,會遍歷堆中全部的對象,而後標記活的對象,在標記完成後,銷燬全部沒有被標記的對象。在標記大型對內存時,可能須要幾百毫秒才能完成一次標記。這就會致使一些性能上的問題。爲了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工做分解爲更小的模塊,可讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓狀況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名爲併發標記。該技術可讓 GC 掃描和標記對象時,同時容許 JS 運行。

清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。

 

7、內存泄露和優化

7.1 什麼是內存泄露?

存泄露是指程序中已分配的堆內存因爲某種緣由未釋放或者沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統奔潰等後果。。

7.2 常見的內存泄露的場景

7.2.1 緩存

js開發時候喜歡用對象的鍵值來緩存函數的計算結果,可是緩存中存儲的鍵越多,長期存活的對象就越多,致使垃圾回收在進行掃描和整理時,對這些對象作了不少無用功。

7.2.2 做用域未釋放(閉包)

var leakArray = []; exports.leak = function () { leakArray.push("leak" + Math.random()); }

模塊在編譯執行後造成的做用域由於模塊緩存的緣由,不被釋放,每次調用 leak 方法,都會致使局部變量 leakArray 不停增長且不被釋放。

閉包能夠維持函數內部變量駐留內存,使其得不到釋放。

 

7.2.3 沒有必要的全局變量

聲明過多的全局變量,會致使變量常駐內存,要直到進程結束纔可以釋放內存。

 

7.2.4 無效的DOM引用

//dom still exist function click(){ // 可是 button 變量的引用仍然在內存當中。 const button = document.getElementById('button'); button.click(); } // 移除 button 元素 function removeBtn(){ document.body.removeChild(document.getElementById('button')); }

 

7.2.5 定時器未清除

// vue 的 mounted 或 react 的 componentDidMount componentDidMount() { setInterval(function () { // ...do something }, 1000) }

vue 或 react 的頁面生命週期初始化時,定義了定時器,可是在離開頁面後,未清除定時器,就會致使內存泄漏。

 

7.2.6 事件監聽爲空白

componentDidMount() {
    window.addEventListener("scroll", function () { // do something... }); }

在頁面生命週期初始化時,綁定了事件監聽器,但在離開頁面後,未清除事件監聽器,一樣也會致使內存泄漏。

 

7.3 內存泄露優化

7.3.1 解除引用

確保佔用最少的內存可讓頁面得到更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據。一旦數據再也不有用,最好經過將其值設置爲 null 來釋放其引用——這個作法叫作解除引用(dereferencing)

function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson("Nicholas"); // 手動解除 globalPerson 的引用 globalPerson = null;

解除一個值的引用並不意味着自動回收該值所佔用的內存。解除引用的真正做用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收

 

7.3.2 提供手動清空變量的方法

var leakArray = []; exports.clear = function () { leakArray = []; }

 

7.3.3 其餘方法

一、在業務不須要的用到的內部函數,能夠重構到函數外,實現解除閉包。

二、避免建立過多的生命週期較長的對象,或者將對象分解成多個子對象。

三、避免過多使用閉包。

四、注意清除定時器和事件監聽器。

五、nodejs中使用stream或buffer來操做大文件,不會受nodejs內存限制。

六、使用redis等外部工具來緩存數據。

 

8、總結

js是一門具備自動回收垃圾收集的編程語言,在瀏覽器中主要是經過標記清除的方法回收垃圾,在nodejs中主要是經過分代回收,Scavenge,標記清除,增量標記等算法來回收垃圾。在平常開發中,有一些不引入注意的書寫方式可能會致使內存泄露,多注意本身代碼規範。

 

9、參考

一、V8的垃圾回收機制與內存限制

二、node 內存限制的問題

三、node內存控制

四、深刻淺出Nodejs

五、javascript高級程序設計

 

原文出處:https://www.cnblogs.com/chengxs/p/10919311.html

相關文章
相關標籤/搜索