摘要:一前言的垃圾回收機制使用垃圾回收機制來自動管理內(nèi)存。垃圾回收器只會針對新生代內(nèi)存區(qū)老生代指針區(qū)以及老生代數(shù)據(jù)區(qū)進行垃圾回收。分別對新生代和老生代使用不同的垃圾回收算法來提升垃圾回收的效率。
V8 實現(xiàn)了準確式 GC,GC 算法采用了分代式垃圾回收機制。因此,V8 將內(nèi)存(堆)分為新生代和老生代兩部分。
一、前言V8的垃圾回收機制:JavaScript使用垃圾回收機制來自動管理內(nèi)存。垃圾回收是一把雙刃劍,其好處是可以大幅簡化程序的內(nèi)存管理代碼,降低程序員的負擔,減少因 長時間運轉(zhuǎn)而帶來的內(nèi)存泄露問題。
但使用了垃圾回收即意味著程序員將無法掌控內(nèi)存。ECMAScript沒有暴露任何垃圾回收器的接口。我們無法強迫其進 行垃圾回收,更無法干預內(nèi)存管理
內(nèi)存管理問題:在瀏覽器中,Chrome V8引擎實例的生命周期不會很長(誰沒事一個頁面開著幾天幾個月不關),而且運行在用戶的機器上。如果不幸發(fā)生內(nèi)存泄露等問題,僅僅會 影響到一個終端用戶。且無論這個V8實例占用了多少內(nèi)存,最終在關閉頁面時內(nèi)存都會被釋放,幾乎沒有太多管理的必要(當然并不代表一些大型Web應用不需 要管理內(nèi)存)。但如果使用Node作為服務器,就需要關注內(nèi)存問題了,一旦內(nèi)存發(fā)生泄漏,久而久之整個服務將會癱瘓(服務器不會頻繁的重啟)。
二、chrome內(nèi)存限制
2.1存在限制Chrome限制了所能使用的內(nèi)存極限(64位為1.4GB,32位為1.0GB),這也就意味著將無法直接操作一些大內(nèi)存對象。
2.2為何限制Chrome之所以限制了內(nèi)存的大小,表面上的原因是V8最初是作為瀏覽器的JavaScript引擎而設計,不太可能遇到大量內(nèi)存的場景,而深層次的原因 則是由于V8的垃圾回收機制的限制。由于V8需要保證JavaScript應用邏輯與垃圾回收器所看到的不一樣,V8在執(zhí)行垃圾回收時會阻塞 JavaScript應用邏輯,直到垃圾回收結(jié)束再重新執(zhí)行JavaScript應用邏輯,這種行為被稱為“全停頓”(stop-the-world)。 若V8的堆內(nèi)存為1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。這樣瀏覽器將在1s內(nèi)失去對用戶的響 應,造成假死現(xiàn)象。如果有動畫效果的話,動畫的展現(xiàn)也將顯著受到影響
三、chrome V8的堆構成V8的堆其實并不只是由老生代和新生代兩部分構成,可以將堆分為幾個不同的區(qū)域:
1、新生代內(nèi)存區(qū):大多數(shù)的對象被分配在這里,這個區(qū)域很小但是垃圾回特別頻繁;
2、老生代指針區(qū):屬于老生代,這里包含了大多數(shù)可能存在指向其他對象的指針的對象,大多數(shù)從新生代晉升的對象會被移動到這里;
3、老生代數(shù)據(jù)區(qū):屬于老生代,這里只保存原始數(shù)據(jù)對象,這些對象沒有指向其他對象的指針;
4、大對象區(qū):這里存放體積超越其他區(qū)大小的對象,每個對象有自己的內(nèi)存,垃圾回收其不會移動大對象;
5、代碼區(qū):代碼對象,也就是包含JIT之后指令的對象,會被分配在這里。唯一擁有執(zhí)行權限的內(nèi)存區(qū);
6、Cell區(qū)、屬性Cell區(qū)、Map區(qū):存放Cell、屬性Cell和Map,每個區(qū)域都是存放相同大小的元素,結(jié)構簡單。
每個區(qū)域都是由一組內(nèi)存頁構成,內(nèi)存頁是V8申請內(nèi)存的最小單位,除了大對象區(qū)的內(nèi)存頁較大以外,其他區(qū)的內(nèi)存頁都是1MB大小,而且按照1MB對 齊。內(nèi)存頁除了存儲的對象,還有一個包含元數(shù)據(jù)和標識信息的頁頭,以及一個用于標記哪些對象是活躍對象的位圖區(qū)。另外每個內(nèi)存頁還有一個多帶帶分配在另外內(nèi) 存區(qū)的槽緩沖區(qū),里面放著一組對象,這些對象可能指向其他存儲在該頁的對象。垃圾回收器只會針對新生代內(nèi)存區(qū)、老生代指針區(qū)以及老生代數(shù)據(jù)區(qū)進行垃圾回收。
四、chrome V8的垃圾回收機制 4.1如何判斷回收內(nèi)容如何確定哪些內(nèi)存需要回收,哪些內(nèi)存不需要回收,這是垃圾回收期需要解決的最基本問題。我們可以這樣假定,一個對象為活對象當且僅當它被一個根對象 或另一個活對象指向。根對象永遠是活對象,它是被瀏覽器或V8所引用的對象。被局部變量所指向的對象也屬于根對象,因為它們所在的作用域?qū)ο蟊灰暈楦鶎?象。全局對象(Node中為global,瀏覽器中為window)自然是根對象。瀏覽器中的DOM元素也屬于根對象。
4.2如何識別指針和數(shù)據(jù)垃圾回收器需要面臨一個問題,它需要判斷哪些是數(shù)據(jù),哪些是指針。由于很多垃圾回收算法會將對象在內(nèi)存中移動(緊湊,減少內(nèi)存碎片),所以經(jīng)常需要進行指針的改寫:
目前主要有三種方法來識別指針:
保守法:將所有堆上對齊的字都認為是指針,那么有些數(shù)據(jù)就會被誤認為是指針。于是某些實際是數(shù)字的假指針,會背誤認為指向活躍對象,導致內(nèi)存泄露(假指針指向的對象可能是死對象,但依舊有指針指向——這個假指針指向它)同時我們不能移動任何內(nèi)存區(qū)域。
編譯器提示法:如果是靜態(tài)語言,編譯器能夠告訴我們每個類當中指針的具體位置,而一旦我們知道對象時哪個類實例化得到的,就能知道對象中所有指針。這是JVM實現(xiàn)垃圾回收的方式,但這種方式并不適合JS這樣的動態(tài)語言
標記指針法:這種方法需要在每個字末位預留一位來標記這個字段是指針還是數(shù)據(jù)。這種方法需要編譯器支持,但實現(xiàn)簡單,而且性能不錯。V8采用的是這種方式。V8將所有數(shù)據(jù)以32bit字寬來存儲,其中最低一位保持為0,而指針的最低兩位為01
4.3 V8回收策略自動垃圾回收算法的演變過程中出現(xiàn)了很多算法,但是由于不同對象的生存周期不同,沒有一種算法適用于所有的情況。所以V8采用了一種分代回收的策 略,將內(nèi)存分為兩個生代:新生代和老生代。
新生代的對象為存活時間較短的對象,老生代中的對象為存活時間較長或常駐內(nèi)存的對象。分別對新生代和老生代使用 不同的垃圾回收算法來提升垃圾回收的效率。對象起初都會被分配到新生代,當新生代中的對象滿足某些條件(后面會有介紹)時,會被移動到老生代(晉升)。
五、新生代算法新生代中的對象一般存活時間較短,使用 Scavenge GC 算法。在Scavenge的具體實現(xiàn)中,主要是采用一種復制的方式的方法--cheney算法。
在新生代空間中,內(nèi)存空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閑的。新分配的對象會被放入 From 空間中,當 From 空間被占滿時,新生代 GC 就會啟動了。算法會檢查 From 空間中存活的對象并復制到 To 空間中,如果有失活的對象就會銷毀。當復制完成后將 From 空間和 To 空間互換,這樣 GC 就結(jié)束了。
六、老生代算法老生代中的對象一般存活時間較長且數(shù)量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。
在講算法前,先來說下什么情況下對象會出現(xiàn)在老生代空間中:
1、新生代中的對象是否已經(jīng)經(jīng)歷過一次 Scavenge 算法,如果經(jīng)歷過的話,會將對象從新生代空間移到老生代空間中。
2、To 空間的對象占比大小超過 25 %。在這種情況下,為了不影響到內(nèi)存分配,會將對象從新生代空間移到老生代空間中。
老生代中的空間很復雜,有如下幾個空間:
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 };
在老生代中,以下情況會先啟動標記清除算法:
1、某一個空間沒有分塊的時候
2、空間中被對象超過一定限制
3、空間不能保證新生代中的對象移動到老生代中
Mark Sweep 是將需要被回收的對象進行標記,在垃圾回收運行時直接釋放相應的地址空間,如下圖所示(紅色的內(nèi)存區(qū)域表示需要被回收的區(qū)域):
Mark Compact 的思想有點像新生代垃圾回收時采取的 Cheney 算法:將存活的對象移動到一邊,將需要被回收的對象移動到另一邊,然后對需要被回收的對象區(qū)域進行整體的垃圾回收。
在這個階段中,會遍歷堆中所有的對象,然后標記活的對象,在標記完成后,銷毀所有沒有被標記的對象。在標記大型對內(nèi)存時,可能需要幾百毫秒才能完成一次標記。這就會導致一些性能上的問題。為了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標志。在增量標記期間,GC 將標記工作分解為更小的模塊,可以讓 JS 應用邏輯在模塊間隙執(zhí)行一會,從而不至于讓應用出現(xiàn)停頓情況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名為并發(fā)標記。該技術可以讓 GC 掃描和標記對象時,同時允許 JS 運行。
清除對象后會造成堆內(nèi)存出現(xiàn)碎片的情況,當碎片超過一定限制后會啟動壓縮算法。在壓縮過程中,將活的對象像一端移動,直到所有對象都移動完成然后清理掉不需要的內(nèi)存。
七、內(nèi)存泄露和優(yōu)化 7.1 什么是內(nèi)存泄露?存泄露是指程序中已分配的堆內(nèi)存由于某種原因未釋放或者無法釋放,造成系統(tǒng)內(nèi)存的浪費,導致程序運行速度減慢甚至系統(tǒng)奔潰等后果。。
7.2 常見的內(nèi)存泄露的場景7.2.1 緩存
js開發(fā)時候喜歡用對象的鍵值來緩存函數(shù)的計算結(jié)果,但是緩存中存儲的鍵越多,長期存活的對象就越多,導致垃圾回收在進行掃描和整理時,對這些對象做了很多無用功。
7.2.2 作用域未釋放(閉包)
var leakArray = []; exports.leak = function () { leakArray.push("leak" + Math.random()); }
模塊在編譯執(zhí)行后形成的作用域因為模塊緩存的原因,不被釋放,每次調(diào)用 leak 方法,都會導致局部變量 leakArray 不停增加且不被釋放。
閉包可以維持函數(shù)內(nèi)部變量駐留內(nèi)存,使其得不到釋放。
7.2.3 沒有必要的全局變量
聲明過多的全局變量,會導致變量常駐內(nèi)存,要直到進程結(jié)束才能夠釋放內(nèi)存。
7.2.4 無效的DOM引用
//dom still exist function click(){ // 但是 button 變量的引用仍然在內(nèi)存當中。 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 的頁面生命周期初始化時,定義了定時器,但是在離開頁面后,未清除定時器,就會導致內(nèi)存泄漏。
7.2.6 事件監(jiān)聽為空白
componentDidMount() { window.addEventListener("scroll", function () { // do something... }); }
在頁面生命周期初始化時,綁定了事件監(jiān)聽器,但在離開頁面后,未清除事件監(jiān)聽器,同樣也會導致內(nèi)存泄漏。
7.3 內(nèi)存泄露優(yōu)化7.3.1 解除引用
確保占用最少的內(nèi)存可以讓頁面獲得更好的性能。而優(yōu)化內(nèi)存占用的最佳方式,就是為執(zhí)行中的代碼只保存必要的數(shù)據(jù)。一旦數(shù)據(jù)不再有用,最好通過將其值設置為 null 來釋放其引用——這個做法叫做解除引用(dereferencing)
function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson("Nicholas"); // 手動解除 globalPerson 的引用 globalPerson = null;
解除一個值的引用并不意味著自動回收該值所占用的內(nèi)存。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運行時將其回收。
7.3.2 提供手動清空變量的方法
var leakArray = []; exports.clear = function () { leakArray = []; }
7.3.3 其他方法
1、在業(yè)務不需要的用到的內(nèi)部函數(shù),可以重構到函數(shù)外,實現(xiàn)解除閉包。
2、避免創(chuàng)建過多的生命周期較長的對象,或者將對象分解成多個子對象。
3、避免過多使用閉包。
4、注意清除定時器和事件監(jiān)聽器。
5、nodejs中使用stream或buffer來操作大文件,不會受nodejs內(nèi)存限制。
6、使用redis等外部工具來緩存數(shù)據(jù)。
八、總結(jié)js是一門具有自動回收垃圾收集的編程語言,在瀏覽器中主要是通過標記清除的方法回收垃圾,在nodejs中主要是通過分代回收,Scavenge,標記清除,增量標記等算法來回收垃圾。在日常開發(fā)中,有一些不引入注意的書寫方式可能會導致內(nèi)存泄露,多注意自己代碼規(guī)范。
九、參考1、V8的垃圾回收機制與內(nèi)存限制
2、node 內(nèi)存限制的問題
3、node內(nèi)存控制
4、深入淺出Nodejs
5、javascript高級程序設計
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/105006.html
摘要:新生代的對象為存活時間較短的對象,老生代中的對象為存活時間較長或常駐內(nèi)存的對象。分別對新生代和老生代使用不同的垃圾回收算法來提升垃圾回收的效率。如果指向老生代我們就不必考慮它了。 這篇文章的所有內(nèi)容均來自 樸靈的《深入淺出Node.js》及A tour of V8:Garbage Collection,后者還有中文翻譯版V8 之旅: 垃圾回收器,我在這里只是做了個記錄和結(jié)合 垃圾回收...
摘要:歡迎來我的個人站點性能優(yōu)化其他優(yōu)化瀏覽器關鍵渲染路徑開啟性能優(yōu)化之旅高性能滾動及頁面渲染優(yōu)化理論寫法對壓縮率的影響唯快不破應用的個優(yōu)化步驟進階鵝廠大神用直出實現(xiàn)網(wǎng)頁瞬開緩存網(wǎng)頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優(yōu)化動 歡迎來我的個人站點 性能優(yōu)化 其他 優(yōu)化瀏覽器關鍵渲染路徑 - 開啟性能優(yōu)化之旅 高性能滾動 scroll 及頁面渲染優(yōu)化 理論 | HTML寫法...
摘要:歡迎來我的個人站點性能優(yōu)化其他優(yōu)化瀏覽器關鍵渲染路徑開啟性能優(yōu)化之旅高性能滾動及頁面渲染優(yōu)化理論寫法對壓縮率的影響唯快不破應用的個優(yōu)化步驟進階鵝廠大神用直出實現(xiàn)網(wǎng)頁瞬開緩存網(wǎng)頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優(yōu)化動 歡迎來我的個人站點 性能優(yōu)化 其他 優(yōu)化瀏覽器關鍵渲染路徑 - 開啟性能優(yōu)化之旅 高性能滾動 scroll 及頁面渲染優(yōu)化 理論 | HTML寫法...
摘要:歡迎來我的個人站點性能優(yōu)化其他優(yōu)化瀏覽器關鍵渲染路徑開啟性能優(yōu)化之旅高性能滾動及頁面渲染優(yōu)化理論寫法對壓縮率的影響唯快不破應用的個優(yōu)化步驟進階鵝廠大神用直出實現(xiàn)網(wǎng)頁瞬開緩存網(wǎng)頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優(yōu)化動 歡迎來我的個人站點 性能優(yōu)化 其他 優(yōu)化瀏覽器關鍵渲染路徑 - 開啟性能優(yōu)化之旅 高性能滾動 scroll 及頁面渲染優(yōu)化 理論 | HTML寫法...
摘要:引擎對堆內(nèi)存中的對象進行分代管理新生代存活周期較短的對象,如臨時變量字符串等。內(nèi)存泄漏對于持續(xù)運行的服務進程,必須及時釋放不再用到的內(nèi)存。 (關注福利,關注本公眾號回復[資料]領取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導) 本周正式開始前端進階的第一期,本周的主題是調(diào)用堆棧,今天是第4天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃...
閱讀 3883·2021-11-25 09:43
閱讀 2278·2021-11-23 10:13
閱讀 902·2021-11-16 11:44
閱讀 2431·2019-08-29 17:24
閱讀 1453·2019-08-29 17:17
閱讀 3533·2019-08-29 11:30
閱讀 2649·2019-08-26 13:23
閱讀 2406·2019-08-26 12:10