Javascript的垃圾回收機(jī)制知多少?
1 寫在前面
本文主要圍繞JS引擎相關(guān)知識,來深入了解底層運(yùn)行邏輯,這對于日常開發(fā)維護(hù)高性能Javascript代碼以及排查代碼性能問題有著很好的幫助。關(guān)于JS引擎底層的垃圾回收機(jī)制,后面才能理解內(nèi)存泄漏的問題以及手動預(yù)防和優(yōu)化,實(shí)現(xiàn)對JS內(nèi)存管理以及內(nèi)存溢出的處理。
- 那么我們需要考慮幾個問題:
- 什么是垃圾回收機(jī)制(GC)?
- 垃圾是怎樣產(chǎn)生的?
- 為什么要進(jìn)行垃圾回收?
- Javascript的內(nèi)存是如何管理的?
- Chrome瀏覽器又是如何進(jìn)行垃圾回收的?
2 內(nèi)存管理
在Javascript編程中,內(nèi)存管理大概分成三個步驟,也是內(nèi)存的生命周期:
- 分配你所需系統(tǒng)內(nèi)存的空間
- 使用分配到的內(nèi)存進(jìn)行讀寫操作
- 不需要使用內(nèi)存時,將空間進(jìn)行釋放和歸還
內(nèi)存的生命周期
與其它手動管理內(nèi)存的語言不一樣的是,在Javascript中,當(dāng)我們創(chuàng)建變量時,系統(tǒng)會給對象進(jìn)行自動分配對應(yīng)的內(nèi)存空間以及閑置資源回收,也就是不需要我們手動進(jìn)行分配。但是,正是因?yàn)槔厥諜C(jī)制導(dǎo)致開發(fā)者有著錯誤的感覺,就是他們不用關(guān)心內(nèi)存管理。
- const name = "yichuan";//給字符串分配棧內(nèi)存
- const age = 18;//給數(shù)值分配棧內(nèi)存
- //給對象以及包含的值分配堆內(nèi)存
- const user = {
- name: "onechuan",
- age: 19
- }
- //給數(shù)組以及包含的值分配堆內(nèi)存
- const arr = ["yichuan","onechuan",18];
- //給函數(shù)對象分配堆內(nèi)存
- function sum(x,y){
- return x + y;
- }
在前面《Javascript的數(shù)據(jù)類型知多少》文中,我們知道了基礎(chǔ)數(shù)據(jù)類型和引用數(shù)據(jù)類型的分配機(jī)制,即:
- 簡單數(shù)據(jù)類型內(nèi)存保存在固定的??臻g中,可直接通過值進(jìn)行訪問
- 引用數(shù)據(jù)類型的值大小不固定,其引用地址保存在??臻g、引用所指向的值保存在堆空間中,需要通過引用進(jìn)行訪問
棧內(nèi)存中的基本數(shù)據(jù)類型,可以直接通過操作系統(tǒng)進(jìn)行處理,而堆內(nèi)存中的引用數(shù)據(jù)類型的值大小不確定,因此需要JS的引擎通過垃圾回收機(jī)制進(jìn)行處理。
3 內(nèi)存回收機(jī)制(GC)
Javascript的V8引擎被限制了內(nèi)存的使用,因此根據(jù)不同操作系統(tǒng)的內(nèi)存大小會不一樣。
V8引擎最初設(shè)計是作為瀏覽器的引擎,并未考慮占據(jù)過多的內(nèi)存空間,隨著web技術(shù)工程化的發(fā)展,占據(jù)了越來越多的內(nèi)存空間。又由于被v8的會回收機(jī)制所限制,這樣就引起了js執(zhí)行的線程被掛起,會影響當(dāng)前執(zhí)行的頁面應(yīng)用性能。
垃圾回收算法:就是垃圾收集器按照固定的時間間隔,周期性地尋找那些不再使用的變量,然后將其清楚或釋放內(nèi)存。但是垃圾回收算法是個不完美的方案,因?yàn)槟硥K內(nèi)存是否還可用,屬于不可預(yù)判的問題,也就意味著單純依靠算法是解決不了的。還有為什么不是實(shí)時的找出無用內(nèi)存并釋放呢?其實(shí)很簡單,實(shí)時開銷太大了。
我們知道了垃圾是如何產(chǎn)生的,那么我們應(yīng)該如何清除呢?在瀏覽器的發(fā)展歷史上有兩種解決策略:
- 標(biāo)記清除
- 引用計數(shù)
標(biāo)記清除
標(biāo)記清除分為:標(biāo)記階段和清除階段。
首先它會遍歷堆內(nèi)存上所有的對象,分別給它們打上標(biāo)記,然后在代碼執(zhí)行過程結(jié)束之后,對所使用過的變量取消標(biāo)記。在清除階段再把具有標(biāo)記的內(nèi)存對象進(jìn)行整體清除,從而釋放內(nèi)存空間。
整個標(biāo)記清除算法大致過程就像下面這樣
- 垃圾收集器在運(yùn)行時會給內(nèi)存中的所有變量都加上一個標(biāo)記
- 然后從各個根對象開始遍歷,把還在被上下文變量引用的變量標(biāo)記去掉標(biāo)記
- 清理所有帶有標(biāo)牌機(jī)的變量,銷毀并回收它們所占用的內(nèi)存空間
- 最后垃圾回收程序做一次內(nèi)存清理
使用標(biāo)記清除策略的最重要的優(yōu)點(diǎn)在于簡單,無非是標(biāo)記和不標(biāo)記的差異。通過標(biāo)記清除之后,剩余的對象內(nèi)存位置是不變的,也會導(dǎo)致空閑內(nèi)存空間是不連續(xù)的,這就造成出現(xiàn)內(nèi)存碎片的問題。內(nèi)存碎片多了后,如果要存儲一個新的需要占據(jù)較大內(nèi)存空間的對象,就會造成影響。對于通過標(biāo)記清除產(chǎn)生的內(nèi)存碎片,還是需要通過標(biāo)記整理策略進(jìn)行解決。
簡而言之:
- 優(yōu)點(diǎn):簡單
- 缺點(diǎn):內(nèi)存碎片化、分配速度慢
標(biāo)記整理
經(jīng)過標(biāo)記清除策略整理后,老生代內(nèi)存中因此產(chǎn)生了許多內(nèi)存碎片,如果不進(jìn)行清理內(nèi)存碎片,就會對存儲造成影響。
標(biāo)記整理(Mark-Compact)算法 就可以有效地解決標(biāo)記清除的兩個缺點(diǎn)。它的標(biāo)記階段和標(biāo)記清除算法沒有什么不同,只是標(biāo)記結(jié)束后,標(biāo)記整理算法會將活著的對象(即不需要清理的對象)向內(nèi)存的一端移動,最后清理掉邊界的內(nèi)存。
引用計數(shù)
引用計數(shù)是一種不常見的垃圾回收策略,其思路就是對每個值都記錄其的引用次數(shù)。具體的:
- 當(dāng)變量進(jìn)行聲明并賦值后,值的引用數(shù)為1。
- 當(dāng)同一個值被賦值給另一個變量時,引用數(shù)+1
- 當(dāng)保存該值引用的變量被其它值覆蓋時,引用數(shù)-1
- 當(dāng)該值的引用數(shù)為0時,表示無法再訪問該值了,此時就可以放心地將其清除并回收內(nèi)存。
- let a = new Object() // 此對象的引用計數(shù)為 1(a引用)
- let b = a // 此對象的引用計數(shù)是 2(a,b引用)
- a = null // 此對象的引用計數(shù)為 1(b引用)
- b = null // 此對象的引用計數(shù)為 0(無引用)
- ... // GC 回收此對象
這種回收策略看起來很方便,但是當(dāng)其進(jìn)行循環(huán)引用時就會出現(xiàn)問題,會造成大量的內(nèi)存不會被釋放。當(dāng)函數(shù)結(jié)束后,兩個對象都不在作用域中,A 和 B 都會被當(dāng)作非活動對象來清除掉,相比之下,引用計數(shù)則不會釋放,也就會造成大量無用內(nèi)存占用,這也是后來放棄引用計數(shù),使用標(biāo)記清除的原因之一。
4 V8對于垃圾回收機(jī)制的優(yōu)化
大多數(shù)瀏覽器都是基于標(biāo)記清除算法,不同的只是在運(yùn)行垃圾回收的頻率具有差異。V8 對其進(jìn)行了一些優(yōu)化加工處理,那接下來我們主要就來看 V8 中對垃圾回收機(jī)制的優(yōu)化。
分代式垃圾回收
V8 的垃圾回收策略主要基于分代式垃圾回收機(jī)制,V8 中將堆內(nèi)存分為新生代和老生代兩區(qū)域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。
新生代的對象為存活時間較短的對象,簡單來說就是新產(chǎn)生的對象,通常只支持 1~8M 的容量,而老生代的對象為存活事件較長或常駐內(nèi)存的對象,簡單來說就是經(jīng)歷過新生代垃圾回收后還存活下來的對象,容量通常比較大。
V8 整個堆內(nèi)存的大小就等于新生代加上老生代的內(nèi)存,對于新老兩塊內(nèi)存區(qū)域的垃圾回收,V8 采用了兩個垃圾回收器來管控。
新生代和老生代
新生代內(nèi)存回收
在64操作系統(tǒng)下分配為32MB,因?yàn)樾律械淖兞看婊顣r間短,不太容易產(chǎn)生太大的內(nèi)存壓力,因此不夠大也是能夠理解。
對于新生代內(nèi)存的回收,通常是通過Scavenge 的算法進(jìn)行垃圾回收,就是將新生代內(nèi)存進(jìn)行一分為二,正在被使用的內(nèi)存空間稱為使用區(qū),而限制狀態(tài)的內(nèi)存空間稱為空閑區(qū)。
新生代內(nèi)存回收的原理是:
- 新加入的對象都會存放在使用區(qū),當(dāng)使用區(qū)快寫滿時就進(jìn)行一次垃圾清理操作。
- 在開始進(jìn)行垃圾回收時,新生代回收器會對使用區(qū)內(nèi)的對象進(jìn)行標(biāo)記
- 標(biāo)記完成后,需要對使用區(qū)內(nèi)的活動對象拷貝到空閑區(qū)進(jìn)行排序
- 而后進(jìn)入垃圾清理階段,將非活動對象占用的內(nèi)存空間進(jìn)行清理
- 最后對使用區(qū)和空閑區(qū)進(jìn)行交換,使用區(qū)->空閑區(qū),空閑區(qū)->使用區(qū)
新生代中的變量如果經(jīng)過回收之后依然一直存在,那么會放入到老生代內(nèi)存中,只要是已經(jīng)經(jīng)歷過一次Scavenge算法回收的,就可以晉升為老生代內(nèi)存的對象。
老生代內(nèi)存回收
當(dāng)然,Scavenge算法也有其適用場景范圍,對于內(nèi)存空間較大的就不適合使用Scavenge算法。此時應(yīng)該使用Mark-Sweep(標(biāo)記清除)和Mark-Compact(標(biāo)記整理)的策略進(jìn)行老生代內(nèi)存中的垃圾回收。
首先是標(biāo)記階段,從一組根元素開始,遞歸遍歷這組根元素,遍歷過程中能到達(dá)的元素稱為活動對象,沒有到達(dá)的元素就可以判斷為非活動對象。清除階段老生代垃圾回收器會直接將非活動對象,也就是數(shù)據(jù)清理掉。
同樣的標(biāo)記清除策略會產(chǎn)生內(nèi)存碎片,因此還需要進(jìn)行標(biāo)記整理策略進(jìn)行優(yōu)化。
5 內(nèi)存泄漏與優(yōu)化
內(nèi)存泄漏,指在JS中已經(jīng)分配內(nèi)存地址的對象由于長時間未進(jìn)行內(nèi)存釋放或無法清除,造成了長期占用內(nèi)存,使得內(nèi)存資源浪費(fèi),最終導(dǎo)致運(yùn)行的應(yīng)用響應(yīng)速度變慢以及最終崩潰的情況。
在代碼中創(chuàng)建對象和變量時會占據(jù)內(nèi)存,但是JS基于自己的內(nèi)存回收機(jī)制是可以確定哪些變量不再需要,并將其進(jìn)行清除。但是,當(dāng)你的代碼中存在邏輯缺陷時,你以為你已經(jīng)不需要,但是程序中還存在這引用,這就導(dǎo)致程序運(yùn)行完后并沒有進(jìn)行合適的回收所占有的內(nèi)存空間。運(yùn)行時間越長占用內(nèi)存越多,隨之出現(xiàn)的問題就是:性能不佳、高延遲、頻繁崩潰。
造成內(nèi)存泄漏的常見原因有:
- 過多的緩存。及時清理過多的緩存。
- 濫用閉包。盡量避免使用大量的閉包。
- 定時器或回調(diào)太多。與節(jié)點(diǎn)或數(shù)據(jù)相關(guān)聯(lián)的計時器不再需要時,DOM節(jié)點(diǎn)對象可以清除,整個回調(diào)函數(shù)也不再需要??墒?,計時器回調(diào)函數(shù)仍然沒有被回收(計時器停止才會被回收)。當(dāng)不需要setTimeout或setInterval時,定時器沒有被清除,定時器的糊掉函數(shù)以及其內(nèi)部依賴的變量都不能被回收,會造成內(nèi)存泄漏。解決方法:在定時器完成工作時,需要手動清除定時器。
- 太多無效的DOM引用。DOM刪除了,但是節(jié)點(diǎn)的引用還在,導(dǎo)致GC無法實(shí)現(xiàn)對其所占內(nèi)存的回收。解決方法:給刪除的DOM節(jié)點(diǎn)引用設(shè)置為null。
- 濫用全局變量。全局變量是根據(jù)定義無法被垃圾回收機(jī)制進(jìn)行收集的,因此需要特別注意臨時存儲和處理大量信息的全局變量。如果必須使用全局變量來存儲數(shù)據(jù),請確保將其指定為null或在完成后重新分配它。解決方法:使用嚴(yán)格模式。
- 從外到內(nèi)執(zhí)行appendChild。此時即使調(diào)用removeChild也無法進(jìn)行釋放內(nèi)存。解決方法:從內(nèi)到外appendChild。
- 反復(fù)重寫同一個數(shù)據(jù)會造成內(nèi)存大量占用,但是IE瀏覽器關(guān)閉后會被釋放。
- 注意程序邏輯,避免編寫『死循環(huán)』之類的代碼。
- DOM對象和JS對象相互引用。
關(guān)于內(nèi)存泄漏,如果你想要更好地排查以及提前避免問題的發(fā)生,最好的解決方法是通過熟練使用Chrome的內(nèi)存剖析工具,多分析多定位Chrome幫你分析保留的內(nèi)存快照,來查看持續(xù)占用大量內(nèi)存的對象。
6 參考文章
- 《「硬核JS」你真的了解垃圾回收機(jī)制嗎》
- 《Javascript核心原理精講》
- 《Javascript高級程序設(shè)計》
7 寫在后面
本篇文章聊了JS的內(nèi)存管理機(jī)制,以及v8垃圾回收機(jī)制,最后我們也分析了一些日常編碼中經(jīng)常遇到內(nèi)存泄漏問題,根據(jù)不同的原因給出對應(yīng)的解決方案。
【編輯推薦】