前方提醒: 篇幅較長,點個贊或者收藏一下,能夠在下一次閱讀時方便查找node
JavaScript是由垃圾回收機制自動進行內存管理的,在咱們編寫代碼的過程當中不須要像C/C++程序員那樣時刻關注內存的分配和釋放問題。在chrome瀏覽器或者node中,這些工做都是交給V8的垃圾回收器自動完成的。程序員
接下來咱們來了解一下V8是如何幫助咱們進行垃圾回收的。算法
在瞭解垃圾回收以前,須要先了解數據是如何保存的。chrome
V8中將內存分爲棧空間和堆空間數組
var a = 1;
var b = {num: 2};
var c = 3;
var d = b;
複製代碼
上述代碼在內存中的保存形式如圖:瀏覽器
咱們所要討論的垃圾回收都是基於堆空間的。markdown
有些數據被使用以後,就不在被須要了,但仍是保存在內存中,這樣的無用數據就是垃圾
。併發
修改上面的代碼:函數
var a = 1;
var b = {num: 2};
var c = 3;
var d = b;
b = {num: 4};
d = b;
複製代碼
此時,棧空間和堆空間變爲以下的狀況:oop
堆空間中地址爲0x001
的對象沒有被任何變量所引用,它就變成了垃圾數據,能夠被垃圾回機制回收。
目前V8採用的可訪問性(reachability)算法來判斷對象是不是活動對象,這個算法是將一些GC Root(根對象)做爲初始存活的對象的集合,從GC Roots對象出發,遍歷GC Root中的全部對象:
GC Root有不少,一般包括瞭如下幾種:
window.test = new Object();
window.test.a = [];
複製代碼
執行上述代碼,內存中的狀況以下圖所示:
再將另外一個對象賦值給a屬性:
window.test.a = new Object();
複製代碼
此時堆中的數組就成爲了爲非活動對象,由於咱們沒法從一個GC Root遍歷到這個數組,垃圾回收機制會把它自動清理。
代際假說是垃圾回收領域中一個重要的術語,它有如下兩個特色:
V8的垃圾回收機制,就是創建在代際假說的基礎之上的。接下來,咱們來分析下 V8 是如何實現垃圾回收的。
在實際的應用中,對象生存週期長短不一,不一樣的垃圾回收算法只針對特定狀況具備最好的效果,針對這種狀況,V8將堆內存分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放生存時間久的對象。V8對兩個區域使用不一樣的垃圾回收器,以便達到最好的效果。
新生代中的大部分對象在內存中存活的週期很短,且回收頻繁,因此須要一個效率很是高的算法。副垃圾回收器使用Scavenge算法進行處理,該算法把新生代空間對半劃分爲兩個區域,一半是From空間,處於使用狀態;一半是To空間,處於閒置狀態。
在新生代分配內存很是容易,咱們只須要保存一個指向內存區的指針,不斷根據新對象的大小進行指針的遞增便可。當該指針到達了新生代內存區的末尾,就須要一次清理。
Scavenge算法是一個空間換時間的複製算法,在佔用空間不大的場景上很是適用。 新加入的對象會存放到From空間,當From空間快被寫滿時,就須要執行一次垃圾清理操做,大體的步驟以下:
Scavenge算法僞代碼:
def scavenge(): // From和To進行交換 swap(fromSpace, toSpace) // 在To空間中維護兩個指針allocationPtr和scanPtr // allocationPtr指向新對象要複製到的地方 // scanPtr指向即將要進行掃描的對象 allocationPtr = toSpace.bottom
scanPtr = toSpace.bottom
// 處理根對象可以直接訪問的對象
for i = 0..len(roots):
root = roots[i]
if inFromSpace(root):
// 將根對象能直接訪問到的對象root複製到To空間中allocationPtr指向的地方,並根據root的大小更新allocationPtr
rootCopy = copyObject(&allocationPtr, root)
// 更新root的地址
setForwardingAddress(root, rootCopy)
roots[i] = rootCopy
// 採用BFS的遍歷方式,開始遍歷全部能到達的對象
while scanPtr < allocationPtr:
obj = object at scanPtr
// 每處理一個對象,scanPtr就向後移動
scanPtr += size(obj)
n = sizeInWords(obj)
// 處理obj的全部子節點
for i = 0..n:
if isPointer(obj[i]) and not inOldSpace(obj[i]):
fromNeighbor = obj[i]
// 若是對象已經被複制到To空間,取它在To空間的地址
if hasForwardingAddress(fromNeighbor):
toNeighbor = getForwardingAddress(fromNeighbor)
// 若是對象不在To空間,將其複製到To空間allocationPtr所指的位置,並根據該對象的大小更新allocationPtr
else:
toNeighbor = copyObject(&allocationPtr, fromNeighbor)
setForwardingAddress(fromNeighbor, toNeighbor)
obj[i] = toNeighbor
// 當scanPtr == allocationPtr時,全部能到達的對象被處理完成,都被複制到了To空間,此時From空間將被清理
def copyObject(*allocationPtr, object):
copy = *allocationPtr
// 根據對象大小更新allocationPtr
*allocationPtr += size(object)
// 將object複製到copy指向的位置,也就是更新以前的allocationPtr位置
memcpy(copy, object, size(object))
return copy
複製代碼
Scavenge算法過程:
var A = {C: {}};
var B = {
D: {
F: {},
G: {}
},
E: {}
};
複製代碼
delete A.C;
複製代碼
開始進行垃圾回收後:
1.將根對象能到達的A、B複製到To,並後移allocationPtr
2.查看scanPtr指向的A對象,因爲A沒有指向其餘對象,因此將scanPtr後移 3.查看scanPtr指向的B對象,發現B可以訪問到D、E,將D和E依次複製到To空間,並移動allocationPtr和scanPtr
4.查看scanPtr指向的D對象,發現D可以訪問到F、G,將F和G依次複製到To空間,並移動allocationPtr和scanPtr
5.查看scanPtr指向的E對象,發現其沒有指向其餘對象,因此將scanPtr後移 6.依次查看F對象和G對象,發現其都沒有指向其餘對象,繼續將scanPtr後移
7.scanPtr和allocationPtr相等時,說明可訪問的對象都已經被處理完成,From空間中剩餘的C變量將被釋放
Scavenge算法的優缺點:
在必定的條件下,須要把存活週期長的對象移動到老生代中,也就是完成了對象的晉升。在從From空間複製到To空間前,會進行下面的步驟:
Scavenge算法會浪費一半空間,所以Scavenge算法並不適用於老生代空間,V8在老生代中的垃圾回收是採用了標記 - 清除(Mark- Sweep)和標記 - 整理(Mark - Compact)兩種算法相結合的方式進行的。
顧名思義, 標記 - 清除算法分爲兩個過程:
因爲清除階段只是清除未被標記的對象,這部分對象在老生代中佔比很小,因此標記 - 清除算法的效率較高。
標記 - 清除算法執行後,內存中會產生大量不連續的內存碎片,這樣會致使內存中沒有足夠的連續內存分配給較大的對象,因而V8又引入了另一種算法:標記 - 整理(Mark - Compact)。
它的標記過程和標記 - 清除算法一致,但接下來標記 - 整理算法不是直接對未標記的對象進行清除,而是讓全部標記過的對象都移向內存的一端,而後直接清理掉這一端以外的內存,起到了整理內存的做用。
因爲標記 - 整理算法須要移動對象,所以它的速度不會很快,V8結合了標記 - 清除和標記 - 整理算法,主要採用標記 - 清除算法,若是空間不足的時候,才使用標記整理。
接下來學習V8是如何優化垃圾回收的執行效率的。
最初,爲了不js邏輯和垃圾回收器看到的狀況不一致的問題,V8採用了垃圾回收時將js執行暫停下來的方式,等待垃圾回收結束後才恢復js的執行,這種行爲被成爲全停頓(Stop-The-World)。
這種方式的劣勢明顯,它會阻塞js的執行,若是垃圾回收佔用的時間較長,就會形成頁面明顯的卡頓。爲了解決全停頓的問題,V8添加並行、增量、併發等技術對垃圾回收機制進行了優化。
接下來分別針對這三種優化方式作出解釋。
並行方式是主線程在執行垃圾回收的任務的同時,使用多個輔助線程來並行處理,這樣就會加快垃圾回收的執行速度。
新生代中副垃圾回收器採用的就是並行方式,它在主線程執行垃圾回收的過程當中,啓動了多個輔助線程來負責垃圾清理操做,這些輔助線程同時將From空間中的數據移動到To區域。但本質上並行方式仍是一種"全停頓",所以還不能知足對性能要求更高的老生代垃圾回收。
2011年,V8 從又引入了增量回收 (Incremental)的方式。垃圾回收器不須要一次執行完整的垃圾回收過程,每次只執行整個垃圾回收過程當中的一小部分工做,好比每次標記一部分數據,能夠參考下圖:
主線程中,js和垃圾回收交替執行,能夠避免單次垃圾回收時間過長形成的卡頓問題。
增量回收 (Incremental)會帶來兩個問題:
針對上面的兩個問題,V8引入了三色標記法和寫屏障機制(Write-barrier)來解決。
爲了解決增量回收中垃圾回收恢復執行時不知道從哪一個位置繼續開始執行的問題,V8採用黑、白、灰三色標記法。
垃圾回收器能夠根據當前是否存在灰色節點來判斷整個標記是否完成。若是沒有灰色節點了,就能夠清理掉白色節點了。若是還有灰色標記,當再次恢復垃圾回收時,便從灰色的節點開始繼續執行。
垃圾回收器將某個節點標記爲黑色後,js代碼執後又爲該黑色節點增長了一個節點,因爲新增節點都是白色,垃圾回收器不會再次將這個白色節點進行標記了,它就會被垃圾回收器回收。
執行了:
window.a = Object()
window.a.b = Object()
window.a.b.c=Object()
複製代碼
又只執行了:
window.a.b = Object() // d
複製代碼
這時就出現了黑色節點指向白色節點的問題,會形成新增節點的誤回收。寫屏障機制就是要強制保證黑色節點不能指向白色節點。
在執行object.field = value
時,V8就插入寫屏障代碼,強制將value標記爲灰色。
// Called after `object.field = value`.
write_barrier(object, field_offset, value) {
if (color(object) == black && color(value) == white) {
set_color(value, grey);
marking_worklist.push(value);
}
}
複製代碼
併發是指主線程不斷執行js代碼,而輔助線程則在後臺徹底執行垃圾回收。
併發回收主要有如下兩個問題:
可是權衡利弊,並行回收這種方式的效率仍是遠高於其餘方式。
並行、增量、併發三種方式在V8的實際應用中不是單獨存在的,V8的主垃圾回收器融合了這三種機制。
本文從V8的數據存儲、內存分代、垃圾回收算法、優化策略幾個方面進行了講解,雖然內容很多,但仍是忽略不少細節,V8垃圾回收機制的細節很是複雜。咱們大多數開發人員在開發JavaScript時不須要需考慮垃圾回收,可是瞭解一些垃圾回收的內部知識能夠幫助咱們考慮內存使用的狀況,更好的進行內存問題的分析、排查和解決,讓咱們在快節奏的技術迭代中把握本質。
參考資料