JavaScript垃圾回收機制原理總結(jié)深入探究
1. 垃圾為何要產(chǎn)生并回收
當(dāng)我們寫代碼時創(chuàng)建一個基本類型、對象、函數(shù)等,都是需要占用內(nèi)存的,JavaScript基本數(shù)據(jù)類型存儲在棧內(nèi)存中,引用數(shù)據(jù)類型存儲在堆內(nèi)存中,但是引用數(shù)據(jù)類型會在棧內(nèi)存中存儲一個實際對象的引用。
比如說我們創(chuàng)建了一個person
對象,然后將person
對象重新賦值:
var person = { name: "橘貓吃不胖", age: 2 } person = [1, 2, 3]; console.log(person); // [ 1, 2, 3 ]
那么原本堆內(nèi)存給person
對象開辟了一個空間來存放,棧內(nèi)存中存放了該引用的地址,但是在下一步中,person
對象成為了一個數(shù)組,也就是說引用地址從原來的對象變成了數(shù)組,原來的引用關(guān)系就沒有了,那么這時原來的對象在堆內(nèi)存中就會成為一個垃圾。
產(chǎn)生的垃圾如果很多,而且一直不清理,堆積起來,就會影響系統(tǒng)的性能,甚至可能造成系統(tǒng)崩潰。
2. 垃圾回收機制
JavaScript中主要的內(nèi)存管理概念是可達(dá)性。
那什么是可達(dá)性呢,比如說定義一個對象:
let person = { name: "橘貓吃不胖", age: 2 } console.log(person.name, person.age); // 橘貓吃不胖 2
person
引用了這個對象,通過person.name
可以獲取到“橘貓吃不胖”的值,通過person.age
可以獲取到2,那么這時就可以認(rèn)為“橘貓吃不胖”和2是可達(dá)的。
person = null; console.log(person.name, person.age); // TypeError: Cannot read properties of null (reading 'name')
如果將person
設(shè)置為null
,那么這兩個值就沒法獲得了,它們就是不可達(dá)的,這時JavaScript垃圾回收機制就會自動從內(nèi)存中將其清除。
那么JavaScript的垃圾回收就是定期找出這些不可達(dá)的對象,然后將其釋放。那么找出這些不可達(dá)的對象有兩種常用的策略:
- 標(biāo)記清除法
- 引用計數(shù)法
2.1 標(biāo)記清除法
標(biāo)記清除法分為標(biāo)記和清除兩個階段,標(biāo)記階段需要從根節(jié)點遍歷內(nèi)存中的所有對象,并為可達(dá)的對象做上標(biāo)記,清除階段則把沒有標(biāo)記的對象(非可達(dá)對象)銷毀。
標(biāo)記清除法的優(yōu)點就是實現(xiàn)簡單。
它的缺點有兩個,首先是內(nèi)存碎片化。這是因為清理掉垃圾之后,未被清除的對象內(nèi)存位置是不變的,而被清除掉的內(nèi)存穿插在未被清除的對象中,導(dǎo)致了內(nèi)存碎片化。
第二個缺點是內(nèi)存分配速度慢。由于空閑內(nèi)存不是一整塊,假設(shè)新對象需要的內(nèi)存是size
,那么需要對空閑內(nèi)存進行一次單向遍歷,找出大于等于size
的內(nèi)存才能為其分配。
標(biāo)記清除算法改進—— 標(biāo)記整理算法
標(biāo)記清除算法的缺點主要在于內(nèi)存清理之后剩余的內(nèi)存位置不變而導(dǎo)致內(nèi)存碎片化,因此可以使用標(biāo)記整理算法改進。
標(biāo)記整理算法的標(biāo)記階段與標(biāo)記清除算法相同,都是從根節(jié)點遍歷內(nèi)存中的所有對象,為可達(dá)的對象打上一個標(biāo)記。但是在標(biāo)記結(jié)束后,標(biāo)記整理算法將這些可達(dá)的對象移向內(nèi)存的一端,然后清理掉邊界的內(nèi)存。
2.2 引用計數(shù)法
引用計數(shù)法主要記錄對象有沒有被其他對象引用,如果沒有被引用,它將被垃圾回收機制回收。它的策略是跟蹤記錄每個變量值被使用的次數(shù),當(dāng)變量值引用次數(shù)為0時,垃圾回收機制就會把它清理掉。
示例代碼如下:
let person = { name: "橘貓吃不胖" }; // { name: "橘貓吃不胖" } 引用次數(shù)為1 let person1 = person; // { name: "橘貓吃不胖" } 引用次數(shù)為2 person = null; // { name: "橘貓吃不胖" } 的引用次數(shù)為1 person1 = null; // { name: "橘貓吃不胖" } 的引用次數(shù)為0
引用計數(shù)法的優(yōu)點是可以實現(xiàn)立即進行垃圾回收。當(dāng)引用計數(shù)在引用值為0時,立即進行垃圾回收,這樣可以達(dá)到立刻垃圾回收的效果。
它的缺點也有兩個,首先它需要一個計數(shù)器,這個計數(shù)器可能要占據(jù)很大的位置,因為我們無法知道被引用數(shù)量的多少。
第二個缺點是無法解決當(dāng)出現(xiàn)循環(huán)引用時無法回收的問題。例如a
引用了b
,b
也引用了a
,兩個對象相互引用,引用計數(shù)不為0,因此無法進行內(nèi)存清理,如下所示:
let a = { name: "橘貓吃不胖" }; let b = { age: 2 }; a.age = b; b.name = a;
3. V8對垃圾回收機制的優(yōu)化——分代式垃圾回收機制
目前大多數(shù)瀏覽器都是基于標(biāo)記清除算法,V8進行了一些優(yōu)化加工處理,采用分代式垃圾回收機制。
3.1 新生代與老生代
原本的垃圾回收機制在每次回收時都要檢查內(nèi)存中所有的對象,這樣的話,一些大、老、存活時間長的對象與新、小、存活時間短的對象檢查頻率相同,但是前者并不需要頻繁進行清理,因此采用分代式垃圾回收機制。
V8中將堆內(nèi)存分為新生代和老生代兩區(qū)域,采用不同的垃圾回收策略進行回收。新生代的對象為存活時間較短的對象,通常只支持1~8M的容量,老生代的對象為存活時間較長或常駐內(nèi)存的對象,容量通常比較大,V8整個堆內(nèi)存的大小就等于新生代加上老生代的內(nèi)存。
3.2 新生代的垃圾回收
新生代垃圾回收策略中,將堆內(nèi)存一分為二,一個是處于使用狀態(tài)的使用區(qū),一個是處于閑置狀態(tài)的空閑區(qū)。
新加入的對象都會存放到使用區(qū),當(dāng)使用區(qū)快滿時,就需要執(zhí)行一次垃圾清理操作,即新生代垃圾回收機制會對使用區(qū)中的活動對象(不需要被清理的對象)做標(biāo)記,標(biāo)記完成之后將這些活動對象復(fù)制到空閑區(qū)并進行排序(避免內(nèi)存碎片化),然后將使用區(qū)清空,原來的空閑區(qū)變?yōu)槭褂脜^(qū),原來的使用區(qū)變?yōu)榭臻e區(qū)。
當(dāng)一個對象經(jīng)過多次復(fù)制后依然存活,它將會被認(rèn)為是生命周期較長的對象,會被移動到老生代的內(nèi)存中,或者一個對象被復(fù)制到空閑區(qū)時,空閑區(qū)占用空間超過了25%,那么該對象也會進入老生代內(nèi)存中。
新生代回收策略——并行回收
JavaScript是單線程的語言,當(dāng)執(zhí)行垃圾回收時,就會阻塞JavaScript腳本的執(zhí)行,垃圾回收結(jié)束后再繼續(xù)JavaScript腳本執(zhí)行,這種情況叫做全停頓(Stop-The-World)。
如果執(zhí)行一次垃圾回收需要100ms,那么腳本執(zhí)行就得暫停100ms,如果執(zhí)行垃圾回收的時間過長,那么就會造成頁面卡頓,帶來不好的用戶體驗。對于這樣的情況,可以采用并行回收的策略。
并行回收指的是在主線程進行垃圾回收時,同時開啟多個輔助線程一起執(zhí)行垃圾回收。比如說一項任務(wù)一個人需要30天才能完成,那么如果安排兩個人甚至多個人,可能10來天甚至更短的時間就完成了。實現(xiàn)并行回收可以大大降低垃圾回收的暫停時間。
新生代對象空間就采用并行策略,在執(zhí)行垃圾回收的過程中,會啟動了多個線程來負(fù)責(zé)新生代中的垃圾清理操作,這些線程同時將對象空間中的數(shù)據(jù)移動到空閑區(qū)域,這個過程中由于數(shù)據(jù)地址會發(fā)生改變,所以還需要同步更新引用這些對象的指針,此即并行回收。
3.3 老生代的垃圾回收
老生代的垃圾回收操作主要就是標(biāo)記清除算法的步驟了,在標(biāo)記階段標(biāo)記所有的可達(dá)對象,清除階段清除掉未被標(biāo)記的對象。又由于該算法會出現(xiàn)內(nèi)存碎片的問題,因此會使用標(biāo)記整理算法來優(yōu)化這個過程。
老生代回收策略——增量標(biāo)記與惰性清理 ①增量標(biāo)記
增量就是將一次標(biāo)記的過程,分成了許多次,每執(zhí)行完一次就讓應(yīng)用邏輯執(zhí)行一會兒,這樣交替多次后完成垃圾回收。但是這會隨之而來新的問題,首先是如何暫停每次標(biāo)記去執(zhí)行JavaScript代碼,還有如果標(biāo)記好的對象在執(zhí)行js中改變了狀態(tài)成為了可達(dá)或者不可達(dá)對象怎么辦,V8對這兩個問題對應(yīng)的解決方案分別是三色標(biāo)記法與寫屏障。
a.三色標(biāo)記法
三色標(biāo)記法使用三種顏色白、灰、黑來標(biāo)記對象的狀態(tài)。白色表示初始狀態(tài),黑色表示已檢查狀態(tài),灰色表示待檢查狀態(tài)。
它的過程為:
1、將所有的對象設(shè)置為白色,然后從root對象出發(fā),將所有可以訪問的對象標(biāo)記為灰色,并用一個數(shù)組緩存起來;
2、遍歷該數(shù)組,每次都把要遍歷的對象標(biāo)記為黑色并移出,并且把他的相鄰節(jié)點都涂成灰色,并放入隊列,直到隊列為空
3、繼續(xù)檢查是否有灰色對象,如果有繼續(xù)放入隊列然后循環(huán),直到所有的可訪問對象都變成黑色
采用三色標(biāo)記法后,程序在恢復(fù)執(zhí)行時可以直接判斷當(dāng)前內(nèi)存中有沒有灰色節(jié)點,如果有灰色節(jié)點,那么從灰色節(jié)點開始繼續(xù)執(zhí)行,如果沒有,直接進入垃圾清理階段。
b.寫屏障
寫屏障可以解決第二個問題,如果執(zhí)行任務(wù)程序時內(nèi)存中標(biāo)記好的對象引用關(guān)系被修改了,比如說黑色對象引用了白色對象,那么它就會將白色對象改成灰色對象,這樣就可以保證下一次標(biāo)記時可以正常進行。
②惰性清理
增量標(biāo)記完成后,就開始清除垃圾。如果當(dāng)前的可用內(nèi)存可以支持快速的執(zhí)行代碼,就沒必要立即清理內(nèi)存,而且清理時沒必要一次性清理完,可以按需清理。
優(yōu)點:大大減少了主線程停頓的時間,讓用戶與瀏覽器交互的過程變得更加流暢
缺點:并沒有減少主線程的總暫停的時間,甚至?xí)晕⒃黾?/p>
老生代回收策略——并發(fā)回收
并發(fā)回收指的是主線程在執(zhí)行JavaScript的過程中,輔助線程能夠在后臺,完成執(zhí)行垃圾回收的操作,輔助線程在執(zhí)行垃圾回收的時候,主線程也可以自由執(zhí)行
垃圾回收機制多次閱讀之后,我受益匪淺,因此寫該文章記錄一下~
到此這篇關(guān)于JavaScript垃圾回收機制原理總結(jié)深入探究的文章就介紹到這了,更多相關(guān)JavaScript垃圾回收內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
面向JavaScript入門初學(xué)者的二叉搜索樹算法教程
二叉搜索樹則是二叉樹的一種,但它只允許你在左側(cè)節(jié)點儲存比父節(jié)點小的值,右側(cè)只允許儲存比父節(jié)點大的值,這篇文章主要給大家介紹了關(guān)于JavaScript二叉搜索樹算法的相關(guān)資料,需要的朋友可以參考下2021-09-09JS實現(xiàn)帶有抽屜效果的產(chǎn)品類網(wǎng)站多級導(dǎo)航菜單代碼
這篇文章主要介紹了JS實現(xiàn)帶有抽屜效果的產(chǎn)品類網(wǎng)站多級導(dǎo)航菜單代碼,涉及JavaScript動態(tài)操作頁面元素屬性的技巧,整體界面效果美觀大方,具有極強的立體感,需要的朋友可以參考下2015-09-09