其實大多數的時候做爲javascript開發者不須要太關心內存的使用和釋放,由於全部的javascript環境都實現了各自的垃圾回收機制(garbage collector(GC)),可是隨着如今的SPA愈來愈多也愈來愈大,愈來愈追求極致的性能漸漸也要求開發者可以適當的瞭解一些垃圾回收機制內部的實現原理,在性能優化和追蹤內存泄漏的時候都可以起到一點幫助。看一段內存泄漏的代碼javascript
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var c = 'a'
function unused() {
if (originalThing) {
console.log("hi");
}
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log('1111');
}
};
};
setInterval(replaceThing,1000)
複製代碼
最先想要去深刻了解javacript GC是看到這道找內存泄漏的題目(具體怎麼內存泄漏,咱們後面在分析).任何一種GC管理都須要作這幾步:java
而常見的識別對象是否須要回收的機制有下面幾種:node
今天就主要看一下V8中GC的具體實現方式web
GC的第一步就是要找出哪些對象須要被回收,哪些不須要。在追蹤分析(Tracing/Reachable)中,認爲能夠被追蹤到(reachability)的對象認爲是不能被回收的對象,剩下的不能被追蹤到的對象就是要回收的對象。 在V8中,追蹤分析會從根對象開始(GC Root)根據指針將全部被能被追蹤到的對象標記爲reachable,javascript中根對象包括調用堆棧和global對象。算法
Generational Hypothesis的意思是大部分的對象在早期就須要被回收。基於這樣的一個假設,有不少的編程語言的垃圾回收機制在設計的時候都是將內存分代,年輕代(young generation),和老代(old generation)。 這裏的代其實就是開闢兩塊space分別存儲剛被分配的對象和通過兩次GC仍是沒有被回收的對象。在V8中有兩個垃圾回收器分別對年輕代和老代進行垃圾回收,Scavenger針對年輕代進行垃圾回收,Major GC針對老代進行垃圾回收,他們的算法也是不一樣的。編程
V8在年輕代的內存space使用的是semi-space算法,也就是說將內存分爲兩半,同時只有一塊的內存能被使用,另一半是徹底空的(或者說這一半內存都是能夠被分配的)。在程序開始執行的時候,將全部的變量都分配能夠被使用的一半內存中(叫作from-space)。當第一次GC開始的時候根據追蹤分析結果,將全部能夠reachable的對象(不能被釋放的對象),所有轉移到剩餘一半能夠被分配的內存中(to-space),這樣from-space中的內存又所有能夠被分配了,這個時候若是又有新申明的對象須要分配內存,就會分配到這一塊內存當中了,最後在轉移完不能被釋放的對象以後,還須要更新引用指針,指向在to-space中最新的地址。瀏覽器
第二次GC開始的時候,在本來的to-space中仍然不能被釋放的對象首先轉移到老代(old generation)的space中,這時候to-space中又所有能夠被分配,重複以前的操做。從from-space中將不能被釋放的對象轉移過來。完成2次GC以後,存貨了兩次的對象如今就在老代裏面了,而存活一次GC的對象如今就在to-space中了,這個to-space也被叫作intermediate generation(中生代).在Scavenger中回收內存有三個過程:標記(追蹤分析),轉移(from-space to to-space),更新指針地址。性能優化
在這種內存回收的機制中,其中一個問題就是轉移對象的時候是會消耗必定性能的,可是根據Generational Hypothesis的假設大部分的對象在早期就會被回收了,這也就意味着只有少部分不能被回收的對象須要被移動,這也意味着若是這個假設不成立,好比咱們的代碼中有不少的閉包致使不少的做用域不能被釋放,那麼將會有大量的對象須要在space之間轉移,是比較浪費性能的。可是相反的,基於大部分對象均可以在早期被回收的假設,若是大部分的對象在早期就能夠被釋放,這種機制的內存回收對這須要在早期就回收的對象實際上是什麼都不須要作的,只須要把不能釋放的少部分對象進行轉移(from-space to to-space),而後在下次分配內存的時候把這部分須要釋放的對象所佔的內存直接覆蓋就能夠了(rewrite dead object)。數據結構
Parallel是V8中調度線程進行內存回收的一種算法,指的是主線程和幫助線程同時進行相同工做量的內存回收,這種算法仍是會中止主線程正在進行的所有工做,可是工做量被平攤到幾個線程以後,理論上時間也被參與線程的數量整除了(加上一些同步調度的開銷)。Scavenger就是使用的這種線程調度機制,當須要進行內存回收的時候,全部的線程得到必定數量的存活的對象引用指針,開始同時將這些存活對象搬運到to-space中。不一樣的線程可能經過不一樣引用路徑訪問到同一個對象,當線程將存活對象轉移到to-space以後,更新完指針地址後,會在from-space的老對象中留下一個forwarding指針,這樣其餘線程找到這個對象以後就能夠經過這個指針來找到新的地址更新引用地址了。閉包
Major GC主要負責老代的內存回收,一樣也是三個過程:標記(追蹤分析),清除,整理壓縮內存。標記這一步和Scavenger同樣經過追蹤分析肯定哪些內存須要被回收,而後在對象被回收之後將被回收的內存加入到free-list這個數據結構中,free-list就像是一個個抽屜,每一個抽屜的大小表明了從這個地址開始能夠被連續分配的內存的大小,當咱們須要在老代中從新分配內存的時候就能夠快速的根據須要分配內存的大小找到一個合適的抽屜把內存進行分配。最後就是進行內存整理,這個就好像是Windows系統整理磁盤同樣,將還沒被倖存的對象利用free-list查找拷貝到其餘的已經被整理完的page中,這樣使小塊的內存碎片也被整理完以後加以利用。跟Scavenger中同樣來回拷貝對象也會有性能的消耗,在V8中只會對高度碎片化的page進行整理,對其餘的page進行清除,這樣在轉移的時候也是同樣的只須要轉移存活的對象就能夠了。
Concurrent一樣也是V8中進行內存回收的線程調度算法,當主線程執行Javascript的時候,幫助線程同步進行內存回收的一些工做。相比Parallel來講這個算法要複雜的多,可能前一毫秒幫助線程在進行GC操做,後一毫秒主線程就改變了這個對象。也有可能幫助線程和主線程同時讀取修改同一個對象。可是這種算法的優點就是當幫助線程進行GC工做的時候,主線程能夠繼續執行JavaScript,徹底不會受到影響。Major GC就是採用的這個算法,當老代的內存到達必定系統自動計算的閥值,就開始進行Major GC,首先每一個幫助線程都會得到必定數量的對象指針,開始針對這些對象進行標記,而後根據對象的引用指針對reachable對象都進行標記,在進行這些標記的同時,主線程仍然在執行JavaScript沒有受到影響。當幫助線程完成標記,或者老代觸及了設定的閥值,主線程也開始參與GC,他首先進行一步快速的標記確認,確保幫助線程在標記的同時主線程修改的對象標記正確(在幫助線程進行標記的時候,若是主線程執行的JavaScript修改了對象會有Write barriers,相似於有個標記)。當主線程確認全部存活的對象都被標記之後,主線程會和幾個子線程一塊兒,對一些內存page進行壓縮和更新指針的工做,不是全部的page都須要進行壓縮(只對高碎片化的進行壓縮),不須要壓縮的利用free-list進行打掃。
在JavaScript中咱們沒辦法用編程的方式主動觸發GC,由於涉及到複雜的線程調度,主動的觸發GC可能會影響正在執行的GC或者下次的GC。對於Scavenger來講,當在新生代中分配內存時,已經沒有空間分配內存致使分配內存失敗時,開始Scavenger垃圾回收,但願能釋放一些內存,而後在嘗試從新分配內存。對於老代來講,開啓內存回收的時機要複雜不少,簡單來講會根據老代中內存佔用的百分比和須要被分配對象的百分比計算出一個合適的閥值,觸及到這個閥值就會開啓老代的垃圾回收。
咱們能夠經過手動設置來設置新生代和老代的space大小:
node --max-old-space-size=1700 index.js
node --max-new-space-size=1024 index.js
複製代碼
雖然咱們經過JavaScript沒辦法主動觸發GC,可是在V8中還有一個空閒GC的機制,他根據被嵌入宿主來決定何時屬於空閒時來執行GC。好比V8在Chrome瀏覽器中,爲了保證動畫渲染的流暢,一秒鐘須要渲染60個幀,至關於16.6毫秒渲染一幀,在16.6毫秒之內渲染完了一幀,好比只花了10毫秒就渲染完了這一幀的動畫,那麼你就有了6.6毫秒的空閒時間能夠執行一些空閒時的GC(在許多新版本的瀏覽器中,開發者也能夠經過requestIdleCallback事件,利用瀏覽器空閒時間來提升性能,有興趣的能夠去了解React 16 fiber的實現)。
那麼在空閒的幾毫秒時間裏能完成一次GC嗎?那就是接下來就要介紹另一種調度算法Incremental了,相比較於其餘調度算法在暫停一次主線程執行一整次完成的GC,Incremental要求把一整個GC中的工做拆成一小塊,和主線程中的JS遞進的執行,或者在主線程有空閒時間的時候執行一小塊GC任務。
不一樣JavaScript引擎實現GC都有不一樣程度的差別,本文主要以V8爲例,有不少地方沒有很是仔細的展開,好比:其實老代裏面不是隻有一塊space,而是有4塊space組成,每塊space存放着不一樣的數據(old space,large object space,matedata space,code space)。垃圾回收設計自己就是一個很複雜的程序,有了GC,讓開發者能夠徹底不用擔憂內存的管理問題。可是適當的瞭解垃圾回收的原理可以幫助咱們更加深刻的理解JavaScript的運行環境,也能夠幫助咱們寫出更高效率的代碼。
最後的最後將以前的內存泄漏代碼一步步的推演:
from-space to-space
theThing (reachable) theThing
replaceThing (reachable) replaceThing
unused originThing
originThing (reachable) => longStr
c someMethod
longStr (reachable)
someMethod (reachable)
複製代碼
from-space to-space old-space
theThing (reachable) theThing originThing -> theThing
replaceThing (reachable) replaceThing theThing -> longStr
unused originThing theThing -> someMethod
originThing (reachable) => longStr => someMethod -> originThing(closure)
c someMethod
longStr (reachable)
someMethod (reachable)
複製代碼
old-space
originThing -> theThing -> longStr & someMethod -> originThing(closure)
originThing -> theThing -> longStr & someMethod -> originThing(closure)
originThing -> theThing -> longStr & someMethod -> originThing(closure)
originThing -> theThing -> longStr & someMethod -> originThing(closure)
originThing -> theThing -> longStr & someMethod -> originThing(closure)
複製代碼
主要致使內存泄漏的緣由是
而後致使在originalThing還引用着老的theThing,theThing中的someMethod引用着originalThing致使所有都reachable沒法釋放。
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var c = 'a'
function unused() {
if (originalThing) {
console.log("hi");
}
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log('1111');
}
};
originalThing = null; //手動釋放局部做用域中的變量
};
setInterval(replaceThing,1000)
複製代碼