Node(V8)的垃圾回收機制

: 聊一聊垃圾回收機制吧。
: 恩,垃圾回收是自動的。

基本概念

GC(Garbage collection)垃圾回收機制。目的是解釋器去判別須要回收的內容,當解釋器認爲一個佔着房子的人已經沒有存在的意義了,就自動收回房子從新對外出租(available)。JS和PY都選擇不相信程序員,選擇本身操控內存問題。node

Node的對象都是分配在堆內存上,V8主要把內存分爲 new-space 和 old-space ,64位系統對應的大小約爲 32MB 和 1400MB(32位系統對應折半)。兩者共同構成Node的總內存(約爲1.4G)。程序員

新生代空間的對象生存週期比較短,容量也比較小,老生代的對象都是「強硬派」,生命力旺盛,容量比較大。Node 不是 HipHop 爲啥非要把內存分這個 「new-school」,「old-school」 ?,就是由於在實際的狀況中,各類垃圾回收策略並不能知足解決不一樣的對象聲明週期長短不一的問題,而只是針對某一種特定狀況很是有用,因此基於分代策略可以根據對象的生命週期不一樣,採用最適合的算法策略進行高效垃圾回收。面試

Node對兩個不一樣生代的不一樣垃圾回收策略構成了整個Node的垃圾回收機制。下面就來詳細說明這兩個不一樣的生代到底是怎麼處理的辣雞的。算法

new-space 與 Scavenge算法

回顧一下 new-space 的特色:對象的生存週期廣泛都比較短。這意味着,「頑固派」對象比較少數組

Scavenge 策略把 new-space 一分爲兩個 「simispace"(半空間),一個叫 處於使用狀態的 From 空間 一個叫閒置的 TO 空間。整個回收的過程就是以下圖:瀏覽器

clipboard.png

引用計數與閉包

那麼在新生代中如何讓GC知道某一個對象已經沒有價值即該對象的生命週期已經結束了呢?緩存

引用計數:所謂引用計數就是跟蹤並記錄每個值被引用的次數,當咱們生命了一個變量而且將一個引用類型賦值給該變量,那麼該引用對象的引用計數加一,若是同一個變量又賦值給了另一個變量,那麼計數再一次增長1。那麼相反的是若是某一個有引用類型值得變量又被賦了另一個值,那麼原先的引用類型的計數就相應的減一,或者當在一個函數執行完畢以後,該函數在執行時所建立的做用域將銷燬,與此同時在該函數做用域中聲明的局部變量所對應的內存空間的引用計數將隨之減一,不出現閉包的狀況下,下一次的垃圾回收機制在被觸發的時候,做用域中的變量所對應的空間就會結束聲明週期。像下面的代碼那樣:閉包

function callOnce(){
    let local = {}
    let foo = {}
    let bar = {a:{},b:{}}
}

那麼所謂閉包,一個在面試中都快被問爛了的概念:),其實說白了就是運用函數能夠做爲參數或者返回值使得一個外部做用域想要訪問內部做用域中的私有變量的一種方式異步

function foo(){
    let local = {a:'ray'}
    return function(){
        return local
    }
}

let bar = foo()

上述代碼就造成了一個閉包,使得一旦有了變量引用了foo函數的返回值函數,就使得該返回值函數得不到釋放,也使得foo函數的做用域得不到釋放,即內存也不會釋放,除非再也不有引用,纔會逐步釋放。函數

old-space 與 標記-清除/標記-整理

分代之中除了 new-space 以外便是 old-space 了 ,分代的目的是爲了針對不一樣的對象生命週期運用不一樣的回收算法。

知足條件晉升到老生代的的對象都有着比較頑強的生機,意味着在老生代中,存活的對象佔有者很大的比重,使用新生代基於複製的策略會有着比較差的效率,此外,新生代中一分爲二的空間策略面對着存活對象較多的狀況也比較不合適。因此在老生代中V8採用了標記-清除與標記-整理這這兩種方式結合的策略。

標記清除分爲標記和清除兩個步驟,先在老生代中遍歷全部的對象,把那些在遍歷過程當中還活着的對象都加上一個標記,在下一步的時候那些沒有被標記的對象就會天然的被回收了。示意圖以下:

clipboard.png -圖片摘自《深刻淺出NodeJS》

黑色的即爲沒有被標記已經死了對象,下一次就會被回收內存空間。

此種方式會致使下一次內存中產生大量碎片,即內存空間不連續,致使內存分配時面對大對象可能會沒法知足,提早出發下一次的垃圾回收機制。因此便又有了一種標記-整理的方式。

對比標記-清除,他多了異步整理的過程,即把標記爲存活的兌現通通整理到內存的一端,完成整理以後直接清除掉另外一端連續的死亡對象空間,以下:

clipboard.png -圖片摘自《深刻淺出NodeJS》

最後,因爲標記-整理這種方式設計大量移動對象操做,致使速度很是慢,多以 V8 主要使用標記-清除的方式,當老生代空間中不足覺得新生代晉升過來的頑固派們分配空間的時候,才使用標記-整理

V8的優化

因爲在進行垃圾回收的時候會致使應用邏輯陷入全停頓的狀態,在進行老生代的回收時,V8引入了 增量式標記,增量式整理,延遲清理等策略,中心思想就是爲了能讓一次垃圾回收過程不那麼佔用太長的應用程序停頓時間,而提出相似於時間片輪轉同樣的策略,讓整個過程「雨露均沾」,GC弄一會,應用程序執行一會。

堆內內存與堆外內存

使用process.memoryUsage()能夠查看node進程的內存使用狀況。單位是字節

{ rss: 22233088,
  heapTotal: 7708672,
  heapUsed: 5095384,
  external: 28898 }

其中 rss 就是 node 進程的常駐內存。V8對內存有限制,可是不一樣於瀏覽器,Node在服務端不免會操做大文件流,因此有了一種跳脫 V8 的內存限制方式就是使用 buffer 進行堆外內存分配。以下代碼:

let showMem = () => {
    let mem = process.memoryUsage()
    //process.memoryUsage()值得單位都是字節,轉化爲兆
    let format = (byte) => {
        return (byte/1024/1024).toFixed(2)+'MB'
    }
    console.log(`rss:${format(mem.rss)}\n heapTotal:${format(mem.heapTotal)}\n heapUsed:${format(mem.heapUsed)}\n external:${format(mem.external)}`);
    console.log('------------------------------------');
}

let useMem = () => {
    let size = 20*1024*1024
    let arr = new Array(size)
    for (let index = 0; index < size; index++) {
        arr[index] = 0      
    }
    return arr
}

let useMemBuffer = () => {
    let size = 20*1024*1024
    let buf = new Buffer(size)
    for (let index = 0; index < size; index++) {
        buf[index] = 0      
    }
    return buf
}

let total = []

for (let index = 0; index < 100; index++) {
    showMem()
    total.push(useMemBuffer())
}

showMem()

下面爲分別調用 useMem()useMemBuffer() 使用數組是經過V8分配堆內存,使用 Buffer 是不使用V8分配堆外內存,分別打印:

clipboard.png

clipboard.png

上圖一表示堆內內存在必定循環次數以後達到溢出邊緣,

圖二可見,externalrss在不斷增大可是其值早就突破了V8的內存上限。是由於堆外內存並非V8進行內存分配的。

下一篇所要討論的緩存算法中,緩存就是一個有可能形成內存泄漏的場景。

參考:

《深刻淺出NodeJS》-- 樸靈
相關文章
相關標籤/搜索