垃圾回收又稱爲 GC(Garbage Collecation)。編寫 JavaScript 程序時,開發者不須要手工跟蹤內存的使用狀況,只要按照標準寫 JavaScript 代碼,JavaScript 程序運行所需內存的分配以及無用內存的回收徹底是自動管理。JavaScript 中自動垃圾回收機制的原理爲:javascript
找出那些再也不使用的變量,而後釋放其佔用的內存。
垃圾收集器會按照固定的時間間隔(或預約的收集時間)週期性地執行此操做。html
局部變量只在函數執行的過程當中存在。
在函數執行過程當中,會爲局部變量在棧內存(或 堆內存)上分配相應的空間來存儲它們的值。在函數中使用這些變量,直至函數執行結束,此時能夠釋放局部變量的內存供未來須要時使用。
以上狀況下,較容易判斷變量是否有存在的必要,更復雜的狀況須要更精細的變量追蹤策略。
JavaScript 中的垃圾收集器必須跟蹤每一個變量是否有用,須要爲再也不有用的變量打上標記,用於未來回收其佔用的內存。標識無用變量的策略一般有兩個:標記清除 和 引用計數 。java
上述過程當中,JavaScript 中變量分爲 基本類型值 和 引用類型值:python
標記清除(mark-and-sweep) 是 JavaScript 中最經常使用的垃圾回收方式。其執行機制以下:git
邏輯上,永遠不能釋放進入環境的變量所佔用的內存,由於執行流進入相應的環境時,可能會用到它們。
標記變量的方式有不少種,可使用標記位的形式記錄變量進入環境,也可單獨爲「進入環境」和「離開環境」添加變量列表來記錄變化。github
標記清除採用的收集策略爲:算法
2008年以前,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript實現使用的均爲 標記清除式的垃圾回收策略,區別可能在垃圾收集的時間間隔。segmentfault
引用計數(reference counting) 是另外一種垃圾收集策略。引用計數的本質是 跟蹤記錄每一個值被引用的次數。其執行機制以下:數組
垃圾收集器下次運行時,會釋放那些引用次數爲零的值所佔用的內存。
引用計數存在一個致命的問題: 循環引用。循環引用是指,對象 A 中包含一個指向對象 B 的指針,而對象 B 中也包含一個指向對象 A 的引用。下面的代碼就是標準的循環引用的例子:瀏覽器
function cycleRefernce() { var objectA = new Object(); var objectB = new Object(); objectA.someOtherObject = objectB; objectB.anotherObject = objectA; } 複製代碼
上述例子中 objectA 和 objectB 經過各自屬性相互引用。按照引用計數的策略,兩個對象的引用次數均爲 2。若採用標記清除策略,函數執行完畢,對象離開做用域就不存在相互引用。但採用引用計數後,函數執行完,兩個對象的引用次數永不爲0,會一直存尊內存中,若屢次調用,致使大量內存得不到回收。
IE8瀏覽器 以前中有一部分對象並非原生的 JavaScript 對象,多是使用 C++ 以 COM 對象的形式實現的(BOM, DOM)。而 COM 對象的垃圾收集機制採用的是 引用計數策略。即便 IE 的 JavaScript 引擎是使用標記清除策略實現的,但 JavaScript 訪問 COM 對象仍然是基於 引用計數策略的。在這種狀況下,只要在 IE 中涉及 COM 對象,就可能存在循環引用的問題。
爲避免出現循環引用,最好在不使用這些對象時,手動斷開 原生 JavaScript 對象 與 DOM 元素之間的鏈接。IE中的循環引用與手動斷開的操做以下所示:
var element = document.getElementById("some_element"); var myObject = new Object(); myObject.element = element; element.someObject = myObject; // 以上 存在循環引用 // ...... // 如下 手工斷開鏈接 myObject.element = null; element.someObject =null; 複製代碼
將變量設置成 null 便可切斷變量與它以前引用的值之間的鏈接。下次垃圾收集器運行時,會刪除這些值並回收它們佔用的內存。
爲解決上述問題,IE9及以上版本把 BOM 和 DOM 對象都轉換成了真正的 JavaScript 對象,避免了兩種垃圾回收算法並存引發的問題。
垃圾收集器是週期運行的,肯定 垃圾收集的時間間隔 是個重要的問題。
IE7以前的垃圾收集器是根據內存分配量運行的,即 256 個變量、4096 個對象(數組)字面量或 64 KB 的字符串。達到這些臨界值的任何一個,垃圾收集器就會運行。因此就致使若是一個腳本含有不少變量,在整個生命週期中一直保有前面臨界值大小的變量,就會頻繁觸發垃圾回收,會存在嚴重的性能問題。
IE7 重寫了垃圾收集例程。新的工做方式爲:觸發垃圾收集的變量分配、字面量和數組元素的臨界值被調整爲 動態修正。初始值與以前版本相同,但若是垃圾收集例程回收的內存低於 15%,則臨界值加倍。若回收內存分配量超過 85%,則臨界值重置回默認值。
在JavaScript腳本中,絕大多數對象的生存期很短,只有部分對象的生存期較長。因此,V8 中的垃圾回收主要使用的是 分代回收 (Generational collection)機制。
V8 引擎將保存對象的 堆 (heap) 進行了分代:
大週期進行的並不頻繁。一次大週期一般是在移動足夠多的對象至老生區後纔會發生。
因爲垃圾清理髮生的比較頻繁,清理的過程必須很快。V8 中的清理過程使用的是 Scavenge 算法,按照 經典的 Cheney 算法 實現的。Scavenge 算法的主要過程是:
算法的僞代碼描述以下:
def scavenge(): swap(fromSpace, toSpace) allocationPtr = toSpace.bottom scanPtr = toSpace.bottom for i = 0..len(roots): root = roots[i] if inFromSpace(root): rootCopy = copyObject(&allocationPtr, root) setForwardingAddress(root, rootCopy) roots[i] = rootCopy while scanPtr < allocationPtr: obj = object at scanPtr scanPtr += size(obj) n = sizeInWords(obj) for i = 0..n: if isPointer(obj[i]) and not inOldSpace(obj[i]): fromNeighbor = obj[i] if hasForwardingAddress(fromNeighbor): toNeighbor = getForwardingAddress(fromNeighbor) else: toNeighbor = copyObject(&allocationPtr, fromNeighbor) setForwardingAddress(fromNeighbor, toNeighbor) obj[i] = toNeighbor def copyObject(*allocationPtr, object): copy = *allocationPtr *allocationPtr += size(object) memcpy(copy, object, size(object)) return copy 複製代碼
若是新生區有某個對象,只有一個指向它的指針,剛好該指針在老生區的對象中,在垃圾回收以前咱們如何得知新生區的該對象是活躍的呢?
爲解決此問題,V8 在寫緩衝區有一個列表,其中記錄了全部老生區對象指向新生區的狀況。新生區對象誕生時不會有指向它的指針,當老生區的對象出現指向新生區對象的指針時,便記錄跨區指向,記錄行爲老是發生在寫操做中。
由於新生區的內存通常都不大,因此使用 Scavenge 算法進行垃圾回收效果比較好。老生區通常佔用內存較大,所以採用的是 標記-清除(Mark-Sweep)算法 與 標記-緊縮(Mark-Compact)算法。
兩種算法都包括兩個階段:標記階段,清除或緊縮階段。
在標記階段,堆上全部的活躍對象都會被發現而且標記。
標記算法的核心是 深度優先搜索,具體過程爲:
- 在標記的初期,位圖是空的,全部對象也都是白的。
- 從根可達的對象會被染色爲灰色,並被放入標記用的一個單獨分配的雙端隊列。
- 標記階段的每次循環,GC會將一個對象從雙端隊列中取出,染色爲黑,而後將它的鄰接對象染色爲灰,並把鄰接對象放入雙端隊列。
- 這一過程在雙端隊列爲空且全部對象都變黑時結束。
- 特別大的對象,如長數組,可能會在處理時分片,以防溢出雙端隊列。若是雙端隊列溢出了,則對象仍然會被染爲灰色,但不會再被放入隊列(這樣他們的鄰接對象就沒有機會再染色了)。
- 所以當雙端隊列爲空時,GC仍然須要掃描一次,確保全部的灰對象都成爲了黑對象。對於未被染黑的灰對象,GC會將其再次放入隊列,再度處理。
標記算法結束後,全部的活躍對象都被染成黑色,全部的死對象還是白的。下一步就能夠清除或者緊縮了。
標記算法執行後,能夠選擇清除 或是緊縮,這兩個算法均可以收回內存,並且二者都做用於頁級(V8 中的內存頁是 1MB 的連續內存塊)
清除算法掃描連續存放的死對象,將其變爲空閒空間,並將其添加到空閒內存鏈表中。清除算法只須要遍歷頁的位圖,搜索連續的白對象。[每一頁都包含數個空閒內存鏈表,其分別表明小內存區(<256字)、中內存區(<2048字)、大內存區(<16384字)和超大內存區(其它更大的內存)]
緊縮算法會嘗試將對象從碎片頁(包含大量小空閒內存的頁)中遷移整合在一塊兒,來釋放內存。這些對象會被遷移到另外的頁上,所以也可能會新分配一些頁。而遷出後的碎片頁就返還給操做系統。
對目標碎片頁中的每一個活躍對象,在空閒內存鏈表中分配一塊其它頁的區域,將該對象複製至新頁,並在碎片頁中的該對象上寫上轉發地址。
遷出過程當中,對象中的舊地址會被記錄下來,這樣在遷出結束後V8會遍歷它所記錄的地址,將其更新爲新的地址。因爲標記過程當中也記錄了不一樣頁之間的指針,此時也會更新這些指針的指向。
對於一個堆很大,活躍對象有不少的腳本時,標記-清除 與 標記-緊縮 的效率可能會很慢,爲減小垃圾回收引發的停頓,引入了 增量標記(Incremental marking) 和 惰性清理(lazy sweeping)。
增量標記容許堆的標記(前面的標記階段)發生在幾回5-10毫秒的小停頓中。增量標記在堆的大小達到必定的閾值時啓用,啓用以後每當必定量的內存分配後,腳本的執行就會停頓並進行一次增量標記。就像普通的標記同樣,增量標記也是一個深度優先搜索,並一樣採用白灰黑機制來分類對象。
增量標記與普通標記的區別是,添加了從黑對象到白對象的指針,爲此須要再次啓用寫屏障中,在記錄 老->新 的同時,記錄 黑->白。在進行清除時,一旦在寫屏障中發現這樣的指針,黑對象會被從新染色爲灰對象,從新放回到雙端隊列中。
惰性清理是指在標記完成後,並不急着釋放空間,無需一次清理全部的頁,垃圾回收器會視狀況逐一清理,直到全部頁都清理完成。
餘下的涉及垃圾回收原理的部分留着後面繼續整理。(平行標記 與 併發標記)
此文章最初記錄在本人2019.02.21的博客 JavaScript垃圾回收機制中,如今轉出來供你們一塊兒學習交流,若有不許確的地方請你們批評指正。