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