https://github.com/ChenMingK/WebKnowledges-Notes
在線閱讀:https://www.kancloud.cn/chenmk/web-knowledges/1080520javascript
對垃圾回收算法而言,其核心思想就是如何判斷內存再也不使用了
比較古老的說法是 引用計數 和 標記清除前端
引用計數算法定義「內存再也不使用」的標準很簡單,就是看一個對象是否有指向它的引用。若是沒有其餘對象指向它了,說明該對象已經再也不需了。java
// 建立一個對象 person,他有兩個指向屬性 age 和 name 的引用 var person = { age: 12, name: 'aaaa' }; person.name = null // 雖然設置爲null,但由於 person 對象還有指向 name 的引用,所以name 不會回收 var p = person person = 1 // 原來的 person 對象被賦值爲 1,但由於有新引用 p 指向原 person 對象,所以它不會被回收 p = null // 原 person 對象已經沒有引用,很快會被回收
由上面能夠看出,引用計數算法是個簡單有效的算法。但它卻存在一個致命的問題:循環引用。若是兩個對象相互引用,儘管他們已再也不使用,垃圾回收器不會進行回收,致使內存泄露。好比下面這樣node
function cycle () { var o1 = {} var o2 = {} o1.a = o2 o2.a = o1 return "Cycle reference!" } cycle()
標記清除算法將「再也不使用的對象」定義爲「沒法達到的對象」。簡單來講,就是從根部(在JS中就是全局對象)出發定時掃描內存中的對象。凡是能從根部到達的對象,都是還須要使用的。那些沒法由根部出發觸及到的對象被標記爲再也不使用,稍後進行回收。
從這個概念能夠看出,沒法觸及的對象包含了沒有引用的對象這個概念(沒有任何引用的對象也是沒法觸及的對象)。但反之未必成立。git
能夠閱讀這篇文章,最近看 《深刻淺出 Node.js》淘到些 V8 垃圾回收機制的介紹。github
在通常的後端開發語言中,基本的內存使用上沒有什麼限制,然而在 Node 中經過 JavaScript 使用內存時會發現只能使用部份內存(64 位系統下約爲 1.4 GB,32 位系統下約爲 0.7 GB)。在這樣的限制下,將會致使 Node 沒法直接操做大內存對象,好比沒法將一個 2GB 的文件讀入內存中進行字符串分析處理。(stream 模塊解決了這個問題)web
形成這個問題的主要緣由在於 Node 基於 V8 構建,V8 的內存管理機制在瀏覽器的應用場景下綽綽有餘,但在 Node 中卻限制了開發者。因此咱們有必要知曉 V8 的內存管理策略。算法
在 V8 中,全部的 JavaScript 對象(object)都是經過堆來進行分配的,Node 提供了 V8 中內存使用量的查看方式,以下:shell
process.memoryUsage() { rss: 21434368, heapTotal: 7159808, heapUsed: 4455120, external: 8224 }
其中,heapTotal 和 heapUsed 是 V8 的堆內存使用狀況,前者是已申請到的堆內存,後者是當前使用的量。若是已申請的堆空閒內存不夠分配新的對象,將繼續申請堆內存,直到堆的大小超過 V8 的限制爲止。
至於 V8 爲什麼要限制堆的大小,主要是內存過大會致使垃圾回收引發 JavaScript 線程暫停執行的時間增加,應用的性能和響應會直線降低,這樣的狀況不只僅是後端服務沒法接受,前端瀏覽器也沒法接受。所以,在當時的考慮下直接限制堆內存是一個好的選擇。
不過 V8 也提供了選項讓咱們打開這個限制,Node 在啓動時能夠傳遞以下的選項:後端
node --max-old-space-size=1700 test.js // 單位爲 MB 設置老生代的內存空間 node --max-new-space-size=1024 test.js // 單位爲 KB 設置新生代的內存空間
上述參數在 V8 初始化時生效,一旦生效就不能再改變。
V8 的垃圾回收策略主要基於分代式垃圾回收機制,在實際應用中,人們發現沒有一種垃圾回收算法可以勝任全部的場景,由於對象的生存週期長短不一,不一樣的算法只能針對特定狀況具備最好的效果。所以,現代的垃圾回收算法按對象的存活時間將內存的垃圾回收進行不一樣的分代,而後分別對不一樣分代的內存施以更高效的算法。
在 V8 中,主要將內存分爲新生代和老生代。新生代的對象爲存活時間較短的對象,老生代的對象爲存活時間較長或常駐內存的對象。
Scavenge 算法
在分代的基礎上,新生代的對象主要經過 Scavenge 算法進行垃圾回收,在 Scavenge 的具體實現中,主要採用了 Cheney 算法。
Cheney 算法是一種採用複製的方式實現的垃圾回收算法,它將堆內存一分爲二,每一部分空間稱爲 semispace。在這兩個 semispace 空間中,只有一個處於使用中,另外一個處於閒置狀態。處於使用狀態的 semispace 空間稱爲 From 空間,處於閒置狀態的空間稱爲 To 空間。
當咱們分配對象時,先是在 From 空間中進行分配。當開始進行垃圾回收時,會檢查 From 空間的存活對象,這些存活對象將被複制到 To 空間中,而非存活對象佔用的空間將被釋放。
完成複製後,From 空間和 To 空間的角色發生對換。
Mark-Sweep & Mark-Compact
老生代中的對象生命週期較長,存活對象佔較大比重,V8 在老生代主要採用 Mark-Sweep 和 Mark-Compact 相結合的方式進行垃圾回收
Mark-Sweep:標記清除,其分爲標記和清除兩個階段。在標記階段遍歷堆中的全部對象,並標記活着的對象,在清除階段只清除沒有被標記的對象。Mark-Sweep 最大的問題在於進行一次標記清除回收後,內存空間會出現不連續的狀態,內存碎片會對後續的內存分配形成問題,好比碎片空間不足以分配一個大對象致使提早觸發垃圾回收。
因而就有了 Mark-Compact:標記整理,簡單來講就是標記完成後加一個整理階段,存活對象往一端移動(合併),整理完成後直接清理掉邊界外的內存。
Incremental Marking
爲了不出現 JavaScript 應用邏輯與垃圾回收器看到的不一致的狀況,垃圾回收的 3 種基本算法須要將應用邏輯暫停下來,待執行完垃圾回收後再恢復執行應用邏輯,這種行爲被稱爲全停頓(stop-the-world)。
對於新生代來講,全停頓的影響不大,可是對於老生代就須要改善。
爲了下降全堆垃圾回收帶來的停頓時間,V8 採用了增量標記(incremental marking)的技術,大概是將本來一口氣停頓完成的動做拆分爲許多小「步進」,每作完一「步進」就讓 JavaScript 應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。
V8 後續還引入了延遲清理(lazy sweeping)、增量式整理(incremental compaction)、併發標記 等技術,感興趣的能夠自行了解。
啓動時添加 --trace_gc
參數,這樣在進行垃圾回收時,將會從標準輸出中打印垃圾回收的日誌信息。
下面是一段示例,執行結束後,將會在 gc.log 文件中獲得全部垃圾回收信息:
node --trace_gc -e "var a = []; for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log
經過在 Node 啓動時使用 --prof 參數,能夠獲得 V8 執行時的性能分析數據:
node --prof test.js