《深入淺出node.js》第四章——記憶體控制(筆記)

zekun發表於2018-09-24

記憶體控制

  1. V8的垃圾回收機制 / 記憶體限制

    1. V8讓JS虛擬機器的效能達到了很快的地步,所以node實現在V8上
    2. V8的記憶體限制:Node中通過JS使用記憶體只能使用部分記憶體(64G下大概1.4GB),所以Node無法直接操作大記憶體物件
    3. V8的物件分配
      1. V8中,所有JS物件都是通過堆進行分配,如果已申請的堆空閒不夠分配新的物件,將繼續申請堆記憶體
      2. 通過process.memoryUsage()可以看到現在的記憶體使用情況
      3. Node在啟動的時候可以傳遞 --max-old-space-size / --max-new-space-size 來調整記憶體限制的大小,只在初始化有效。
    4. V8 的垃圾回收機制
      1. V8主要的垃圾回收演算法:分代式垃圾回收機制
        1. V8中將記憶體分為新生代和老生代,新生代的物件存活時間短,老生代是存存活活時間長/常駐記憶體的物件
        2. 新生代中的物件主要通過scavenge演算法進行垃圾回收,而在scavenge的具體實現使用了Cheney演算法
          1. 將對記憶體一分為二,每一部分空間成為semi space。
          2. 2個semispace只有一個在使用,一個處於閒置狀態。處於使用狀態的semi space空間稱為from空間,處於閒置狀態的空間成為To空間。
          3. 分配物件的時候,先從from空間中開始分配
          4. 垃圾回收時,會檢查from空間中的存活物件,存活物件唄複製到To空間,不存活的物件佔用的空間將會被釋放。
          5. to和from空間 交換
          6. 總之,這個演算法的意思就是將存活物件在兩個semi space空間中進行復制/翻轉
          7. 這個演算法犧牲了空間,換取時間;之所以採用這個演算法是因為新生代中物件的生命週期很短,佔用的記憶體也少
          8. 當一個物件經過多次複製和轉換依然存活,它會被認為是生命週期較長的物件,從而晉升到老生代中。而晉升的套件一般是2個
            1. 物件是否經歷過scavenge回收,如果有,就晉升
            2. To空間的記憶體佔用比是否超過比例,如果超過,直接晉升
        3. 老生代中常用Mark - Sweep(標記清除)& Mark - compact(標記整理)方法
          1. mark-sweep 標記清除
            1. 遍歷每個物件,並標記存活的物件,清楚階段只清楚沒有被標記的物件。
            2. 缺點:很容易出現清理之後記憶體空間不連續,這種記憶體碎片對後續的記憶體分配造成問題,因為很可能出現需要分配一個打碎片,所有的碎片空間都無法完成此次分配,就會提前觸發垃圾回收
          2. mark-compact 標記整理
            1. 標記每個物件,將活著的物件往一端移動,移動完成之後,直接清理掉邊界外的記憶體。
          3. incremental Marking 增量標記,減少垃圾回收的停頓時間
            1. 因為每次回收的時候,JS會阻塞,所以把大的垃圾回收工程拆分一小步一小步進行,比如增量標記和增量清理,還有延遲清理(lazy sweeping)
    5. 檢視垃圾回收日誌
      1. 啟動node的時候新增 --trace-gc引數
  2. 高效使用記憶體

    1. 作用域退出/不再使用了 就會釋放這個作用域裡的變數
      1. 變數的主動釋放
        1. 如果變數是全域性變數,物件將常駐在記憶體。刪除手段有
          1. delete操作刪除引用關係
          2. 重新賦值,讓舊物件脫離引用關係/ 其實直接設定成空/null不行嗎
      2. 閉包(closure)
        1. 閉包導致閉包自身作用域不得到釋放
  3. 記憶體指標

    1. 檢視記憶體使用情況:通過process.memoryUsage()可以看到現在的記憶體使用情況
    2. 檢視程式的記憶體使用,process.memoryUsage()
    3. 檢視系統的記憶體佔用,os模組的totalmem() 和 freemem() 用來檢視系統的總記憶體和限制記憶體
    4. 堆外記憶體:我們把不是通過V8分配的記憶體叫做 堆外記憶體
  4. 記憶體洩漏

    1. 造成記憶體洩漏的原因常見的有
      1. 快取
      2. 佇列消費不及時
      3. 作用域未釋放
    2. 記憶體 != 快取,慎重
      1. 一旦命中快取,就可以節省一次I/O的時間;但是一個物件被當作快取使用,意味著它常駐在老生代中;快取中儲存的鍵越多,長期存活的物件也就越多,那麼垃圾回收舊會做無用功
      2. 快取限制策咯:限制快取的無限增長
        1. 作者寫過一個limitablemap模組——P128
      3. 快取的解決方案
        1. 直接將記憶體作為快取的方案,除了快取大小的顧慮外,還要考慮程式之間無法共享記憶體,程式內使用快取將導致快取不可避免的有重複,浪費物理空間
        2. 解決方案:採用程式外的快取,程式自身不儲存狀態
          1. 將快取轉移到外部,減少常駐記憶體的物件的數量,讓垃圾回收變得高效
          2. 程式之間可以共享快取
        3. 常見的快取方案( 有客戶端)
          1. Redis
          2. Memcached
    3. 關注佇列狀態,因為也有可能造成記憶體洩漏
  5. 記憶體洩漏排查

    1. 常見工具:v8-profiler(3年沒維護了),node-heapdump,node-mtrace,dtrace,node-memwatch
  6. 大記憶體應用

    1. 使用stream模組處理大檔案(由於V8記憶體限制,我們無法通過fs.readFile()和fs.writeFile()直接對大檔案進行操作)

    2. 使用fs.createReadStream() / fs.createWriteStream()方法通過流的方式實現對大檔案的操作

      1. var reader = fs.createReadStream('in.txt');
        var writer = fs.createWriteStream('out.txt');
        reader.on('data',function(chunk){
            writer.write(chunk);
        })
        reader.on('end',function(){
            writer.end;
        })
        
        //利用es6 中的pipe,簡寫後
        
        var reader = fs.createReadStream('in.txt');
        var writer = fs.createWriteStream('out.txt');
        reader.pipe(writer);
        複製程式碼

相關文章