咱們知道,JavaScript之因此能在瀏覽器環境和NodeJS環境運行,都是由於有V8引擎在幕後保駕護航。從編譯、內存分配、運行以及垃圾回收等整個過程,都離不開它。html
在寫這篇文章以前,我也在網上看了不少博客,包括一些英文原版的內容,因而想經過這篇文章來作一個概括整理,文中加入了我本身的思考,以及純手工製做流程圖~~node
但願這篇文章能幫到你,同時本文也會收錄到我本身的我的網站。算法
在C語言和C++語言中,咱們若是想要開闢一塊堆內存的話,須要先計算須要內存的大小,而後本身經過malloc函數去手動分配,在用完以後,還要時刻記得用free函數去清理釋放,不然這塊內存就會被永久佔用,形成內存泄露。segmentfault
可是咱們在寫JavaScript的時候,卻沒有這個過程,由於人家已經替咱們封裝好了,V8引擎會根據你當前定義對象的大小去自動申請分配內存。瀏覽器
不須要咱們去手動管理內存了,因此天然要有垃圾回收,不然的話只分配不回收,豈不是沒多長時間內存就被佔滿了嗎,致使應用崩潰。微信
垃圾回收的好處是不須要咱們去管理內存,把更多的精力放在實現複雜應用上,但壞處也來自於此,不用管理了,就有可能在寫代碼的時候不注意,形成循環引用等狀況,致使內存泄露。架構
因爲V8最開始就是爲JavaScript在瀏覽器執行而打造的,不太可能遇到使用大量內存的場景,因此它能夠申請的最大內存就沒有設置太大,在64位系統下大約爲1.4GB,在32位系統下大約爲700MB。異步
在NodeJS環境中,咱們能夠經過process.memoryUsage()來查看內存分配。ide
process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個字段,含義以下:函數
rss(resident set size):全部內存佔用,包括指令區和堆棧 heapTotal:V8引擎能夠分配的最大堆內存,包含下面的 heapUsed heapUsed:V8引擎已經分配使用的堆內存 external: V8管理C++對象綁定到JavaScript對象上的內存
以上全部內存單位均爲字節(Byte)。
若是說想要擴大Node可用的內存空間,可使用Buffer等堆外內存內存,這裏不詳細說明了,你們有興趣能夠去看一些資料。
下面是Node的總體架構圖,有助於你們理解上面的內容:
Node Standard Library: 是咱們天天都在用的標準庫,如Http, Buffer 模塊 Node Bindings: 是溝通JS 和 C++的橋樑,封裝V8和Libuv的細節,向上層提供基礎API服務 第三層是支撐 Node.js 運行的關鍵,由 C/C++ 實現: 1. V8 是Google開發的JavaScript引擎,提供JavaScript運行環境,能夠說它就是 Node.js 的發動機 2. Libuv 是專門爲Node.js開發的一個封裝庫,提供跨平臺的異步I/O能力 3. C-ares:提供了異步處理 DNS 相關的能力 4. http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數據壓縮等其餘的能力
當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲「離開環境」。
可使用任何方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表及一個「離開環境的」變量列表來跟蹤哪一個變量發生了變化。如何標記變量並不重要,關鍵在於採起什麼策略。
目前,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除式的垃圾回收策略(或相似的策略),只不過垃圾收集的時間間隔互有不一樣。
活動對象就是上面的root,若是不清楚活動對象的能夠先查一下資料,當一個對象和其關聯對象再也不經過引用關係被當前root引用了,這個對象就會被垃圾回收。
引用計數的垃圾收集策略不太常見。含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。
若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量改變了引用對象,則該值引用次數減1。
當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。
這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲0的值所佔用的內存。
Netscape Navigator 3.0是最先使用引用計數策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循環引用。
循環引用是指對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用,看個例子:
function foo () { var objA = new Object(); var objB = new Object(); objA.otherObj = objB; objB.anotherObj = objA; }
這個例子中,objA和objB經過各自的屬性相互引用,也就是說,這兩個對象的引用次數都是2。
在採用標記清除策略的實現中,因爲函數執行後,這兩個對象都離開了做用域,所以這種相互引用不是問題。
但在採用引用次數策略的實現中,當函數執行完畢後,objA和objB還將繼續存在,由於它們的引用次數永遠不會是0。
加入這個函數被重複屢次調用,就會致使大量內存沒法回收。爲此,Netscape在Navigator 4.0中也放棄了引用計數方式,轉而採用標記清除來實現其垃圾回收機制。
還要注意的是,咱們大部分人時刻都在寫着循環引用的代碼,看下面這個例子,相信你們都這樣寫過:
var el = document.getElementById('#el'); el.onclick = function (event) { console.log('element was clicked'); }
咱們爲一個元素的點擊事件綁定了一個匿名函數,咱們經過event參數是能夠拿到相應元素el的信息的。
你們想一想,這是否是就是一個循環引用呢?
el有一個屬性onclick引用了一個函數(其實也是個對象),函數裏面的參數又引用了el,這樣el的引用次數一直是2,即便當前這個頁面關閉了,也沒法進行垃圾回收。
若是這樣的寫法不少不少,就會形成內存泄露。咱們能夠經過在頁面卸載時清除事件引用,這樣就能夠被回收了:
var el = document.getElementById('#el'); el.onclick = function (event) { console.log('element was clicked'); } // ... // ... // 頁面卸載時將綁定的事件清空 window.onbeforeunload = function(){ el.onclick = null; }
自動垃圾回收有不少算法,因爲不一樣對象的生存週期不一樣,因此沒法只用一種回收策略來解決問題,這樣效率會很低。
因此,V8採用了一種代回收的策略,將內存分爲兩個生代:新生代(new generation)和老生代(old generation)。
新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象,分別對新老生代採用不一樣的垃圾回收算法來提升效率,對象最開始都會先被分配到新生代(若是新生代內存空間不夠,直接分配到老生代),新生代中的對象會在知足某些條件後,被移動到老生代,這個過程也叫晉升,後面我會詳細說明。
默認狀況下,32位系統新生代內存大小爲16MB,老生代內存大小爲700MB,64位系統下,新生代內存大小爲32MB,老生代內存大小爲1.4GB。
新生代平均分紅兩塊相等的內存空間,叫作semispace,每塊內存大小8MB(32位)或16MB(64位)。
新生代存的都是生存週期短的對象,分配內存也很容易,只保存一個指向內存空間的指針,根據分配對象的大小遞增指針就能夠了,當存儲空間快要滿時,就進行一次垃圾回收。
新生代採用Scavenge垃圾回收算法,在算法實現時主要採用Cheney算法。
Cheney算法將內存一分爲二,叫作semispace,一塊處於使用狀態,一塊處於閒置狀態。
處於使用狀態的semispace稱爲From空間,處於閒置狀態的semispace稱爲To空間。
我畫了一套詳細的流程圖,接下來我會結合流程圖來詳細說明Cheney算法是怎麼工做的。
垃圾回收在下面我統稱爲 GC(Garbage Collection)。
step1. 在From空間中分配了3個對象A、B、C
step2. GC進來判斷對象B沒有其餘引用,能夠回收,對象A和C依然爲活躍對象
step3. 將活躍對象A、C從From空間複製到To空間
step4. 清空From空間的所有內存
step5. 交換From空間和To空間
step6. 在From空間中又新增了2個對象D、E
step7. 下一輪GC進來發現對象D沒有引用了,作標記
step8. 將活躍對象A、C、E從From空間複製到To空間
step9. 清空From空間所有內存
step10. 繼續交換From空間和To空間,開始下一輪
經過上面的流程圖,咱們能夠很清楚的看到,進行From和To交換,就是爲了讓活躍對象始終保持在一塊semispace中,另外一塊semispace始終保持空閒的狀態。
Scavenge因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上有優異的體現。Scavenge的缺點是隻能使用堆內存的一半,這是由劃分空間和複製機制所決定的。
因爲Scavenge是典型的犧牲空間換取時間的算法,因此沒法大規模的應用到全部的垃圾回收中。但咱們能夠看到,Scavenge很是適合應用在新生代中,由於新生代中對象的生命週期較短,偏偏適合這個算法。
當一個對象通過屢次複製仍然存活時,它就會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老生代中,採用新的算法進行管理。
對象重新生代移動到老生代的過程叫做晉升。
對象晉升的條件主要有兩個:
在老生代中,存活對象佔較大比重,若是繼續採用Scavenge算法進行管理,就會存在兩個問題:
因此,V8在老生代中主要採用了Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收。
Mark-Sweep是標記清除的意思,它分爲標記和清除兩個階段。
與Scavenge不一樣,Mark-Sweep並不會將內存分爲兩份,因此不存在浪費一半空間的行爲。Mark-Sweep在標記階段遍歷堆內存中的全部對象,並標記活着的對象,在隨後的清除階段,只清除沒有被標記的對象。
也就是說,Scavenge只複製活着的對象,而Mark-Sweep只清除死了的對象。活對象在新生代中只佔較少部分,死對象在老生代中只佔較少部分,這就是兩種回收方式都能高效處理的緣由。
咱們仍是經過流程圖來看一下:
step1. 老生代中有對象A、B、C、D、E、F
step2. GC進入標記階段,將A、C、E標記爲存活對象
step3. GC進入清除階段,回收掉死亡的B、D、F對象所佔用的內存空間
能夠看到,Mark-Sweep最大的問題就是,在進行一次清除回收之後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題。
若是出現須要分配一個大內存的狀況,因爲剩餘的碎片空間不足以完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。
爲了解決Mark-Sweep的內存碎片問題,Mark-Compact就被提出來了。
Mark-Compact是標記整理的意思,是在Mark-Sweep的基礎上演變而來的。Mark-Compact在標記完存活對象之後,會將活着的對象向內存空間的一端移動,移動完成後,直接清理掉邊界外的全部內存。以下圖所示:
step1. 老生代中有對象A、B、C、D、E、F(和Mark—Sweep同樣)
step2. GC進入標記階段,將A、C、E標記爲存活對象(和Mark—Sweep同樣)
step3. GC進入整理階段,將全部存活對象向內存空間的一側移動,灰色部分爲移動後空出來的空間
step4. GC進入清除階段,將邊界另外一側的內存一次性所有回收
在V8的回收策略中,Mark-Sweep和Mark-Conpact二者是結合使用的。
因爲Mark-Conpact須要移動對象,因此它的執行速度不可能很快,在取捨上,V8主要使用Mark-Sweep,在空間不足以對重新生代中晉升過來的對象進行分配時,才使用Mark-Compact。
V8的垃圾回收機制分爲新生代和老生代。
新生代主要使用Scavenge進行管理,主要實現是Cheney算法,將內存平均分爲兩塊,使用空間叫From,閒置空間叫To,新對象都先分配到From空間中,在空間快要佔滿時將存活對象複製到To空間中,而後清空From的內存空間,此時,調換From空間和To空間,繼續進行內存分配,當知足那兩個條件時對象會重新生代晉升到老生代。
老生代主要採用Mark-Sweep和Mark-Compact算法,一個是標記清除,一個是標記整理。二者不一樣的地方是,Mark-Sweep在垃圾回收後會產生碎片內存,而Mark-Compact在清除前會進行一步整理,將存活對象向一側移動,隨後清空邊界的另外一側內存,這樣空閒的內存都是連續的,可是帶來的問題就是速度會慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact二者共同進行管理的。
以上就是本文的所有內容,書寫過程當中參考了不少中外文章,參考書籍包括樸大大的《深刻淺出NodeJS》以及《JavaScript高級程序設計》等。咱們這裏並無對具體的算法實現進行探討,感興趣的朋友能夠繼續深刻研究一下。
最後,謝謝你們可以讀到這裏,若是文中有任何不明確或錯誤的地方,歡迎給我留言~~
https://medium.com/@_lrlna/ga...
http://alinode.aliyun.com/blo...
http://www.ruanyifeng.com/blo...
https://segmentfault.com/a/11...