🤔️ 什麼,項目構建時內存溢出了?瞭解一下 node 內存限制

背景

在以前的一篇文章中, 咱們遇到了一個項目在構建時內存溢出的問題。vue

當時的解決方案是: 直接調大 node 的內存限制,避免達到內存上限。node

今天聽同事分享了一個新方法,以爲不錯, 特此記錄, 順便分享給你們, 但願對你們有所幫助。webpack

正文

直接上報錯示意圖:web

提示已經很明顯: Javascript Heap out of memory.算法

看到內存溢出這個關鍵字,咱們通常都會考慮到是由於 Node.js 內存不夠致使的。segmentfault

但 Node 進程的內存限制會是多少呢?閉包

在網上查閱了到以下描述:oop

Currently, by default V8 has a memory limit of 512mb on 32-bit systems, and 1gb on 64-bit systems. The limit can be raised by setting --max-old-space-size to a maximum of ~1gb (32-bit) and ~1.7gb (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.

翻譯一下:post

當前,默認狀況下,V8在32位系統上的內存限制爲512mb,在64位系統上的內存限制爲1gb。學習

能夠經過將--max-old-space-size設置爲最大〜1gb(32位)和〜1.7gb(64位)來提升此限制,可是若是達到內存限制, 建議您將單個進程拆分爲多個工做進程

若是你想知道本身電腦的內存限制有多大, 能夠直接把內存撐爆, 看報錯。

運行以下代碼:

// Small program to test the maximum amount of allocations in multiple blocks.
// This script searches for the largest allocation amount.

// Allocate a certain size to test if it can be done.
function alloc (size) {
  const numbers = size / 8;
  const arr = []
  arr.length = numbers; // Simulate allocation of 'size' bytes.
  for (let i = 0; i < numbers; i++) {
    arr[i] = i;
  }
  return arr;
};

// Keep allocations referenced so they aren't garbage collected.
const allocations = []; 

// Allocate successively larger sizes, doubling each time until we hit the limit.
function allocToMax () {
  console.log("Start");

  const field = 'heapUsed';
  const mu = process.memoryUsage();

  console.log(mu);

  const gbStart = mu[field] / 1024 / 1024 / 1024;

  console.log(`Start ${Math.round(gbStart * 100) / 100} GB`);

  let allocationStep = 100 * 1024;

  // Infinite loop
  while (true) {
    // Allocate memory.
    const allocation = alloc(allocationStep);
    // Allocate and keep a reference so the allocated memory isn't garbage collected.
    allocations.push(allocation);
    // Check how much memory is now allocated.
    const mu = process.memoryUsage();
    const gbNow = mu[field] / 1024 / 1024 / 1024;

    console.log(`Allocated since start ${Math.round((gbNow - gbStart) * 100) / 100} GB`);
  }

  // Infinite loop, never get here.
};

allocToMax();

不出意外, 你將喜提以下報錯:

個人電腦是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,這段代碼大概在 1.6 GB 左右內存時候拋出異常。

那咱們如今知道 Node Process 確實是有一個內存限制的, 那咱們就來增大它的內存限制再試一下。

node --max-old-space-size=6000 來運行這段代碼,獲得以下結果:

內存達到 4.6G 的時候也溢出了。

你可能會問, node 不是有內存回收嗎?這個咱們在下面會講。

使用這個參數:node --max-old-space-size=6000, 咱們增長的內存中老生代區域的大小,比較暴力。

就像上文中提到的: 若是達到內存限制, 建議您將單個進程拆分爲多個工做進程

這個項目是一個 ts 項目,ts 文件的編譯是比較佔用內存的,若是把這部分獨立成一個單獨的進程, 狀況也會有所改善。

由於 ts-loader 內部調用了 tsc,在使用 ts-loader 時,會使用 tsconfig.js配置文件。

當項目中的代碼變的愈來愈多,體積也愈來愈龐大時,項目編譯時間也隨之增長。

這是由於 Typescript 的語義檢查器必須在每次重建時檢查全部文件

ts-loader 提供了一個 transpileOnly 選項,它默認爲 false,咱們能夠把它設置爲 true,這樣項目編譯時就不會進行類型檢查,也不會輸出聲明文件。

對一下 transpileOnly 分別設置 falsetrue 的項目構建速度對比:

  • 當 transpileOnly 爲 false 時,總體構建時間爲 4.88s.
  • 當 transpileOnly 爲 true 時,總體構建時間爲 2.40s.

雖然構建速度提高了,可是有了一個弊端: 打包編譯不會進行類型檢查

好在官方推薦了這樣一個插件, 提供了這樣的能力: fork-ts-checker-webpack-plugin

官方示例的使用也很是簡單:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
  ...
  plugins: [
    new ForkTsCheckerWebpackPlugin()
  ]
}

在我這個實際的項目中,vue.config.js 修改以下:

configureWebpack: config => {
 // get a reference to the existing ForkTsCheckerWebpackPlugin
 const existingForkTsChecker = config.plugins.filter(
   p => p instanceof ForkTsCheckerWebpackPlugin,
 )[0];

// remove the existing ForkTsCheckerWebpackPlugin
// so that we can replace it with our modified version
 config.plugins = config.plugins.filter(
   p => !(p instanceof ForkTsCheckerWebpackPlugin),
 );

 // copy the options from the original ForkTsCheckerWebpackPlugin
 // instance and add the memoryLimit property
 const forkTsCheckerOptions = existingForkTsChecker.options;
 
 forkTsCheckerOptions.memoryLimit = 4096;
 
 config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions));
}

修改以後, 構建就成功了。

unnamed.gif

關於垃圾回收

在 Node.js 裏面,V8 自動幫助咱們進行垃圾回收, 讓咱們簡單看一下V8中如何處理內存。

一些定義

  • 常駐集大小:是RAM中保存的進程所佔用的內存部分,其中包括:

    1. 代碼自己
  • stack:包含原始類型和對對象的引用
  • 堆:存儲引用類型,例如對象,字符串或閉包
  • 對象的淺層大小:對象自己持有的內存大小
  • 對象的保留大小:刪除對象及其相關對象後釋放的內存大小

垃圾收集器如何工做

垃圾回收是回收由應用程序再也不使用的對象所佔用的內存的過程。

一般,內存分配很便宜,而內存池用完時收集起來很昂貴。

若是沒法從根節點訪問對象,則該對象是垃圾回收的候選對象,所以該對象不會被根對象或任何其餘活動對象引用。

根對象能夠是全局對象,DOM元素或局部變量。

堆有兩個主要部分,即 New SpaceOld Space

新空間是進行新分配的地方。

在這裏收集垃圾的速度很快,大小約爲1-8MB

留存在新空間中的物體被稱爲新生代

在新空間中倖存下來的物體被提高的舊空間-它們被稱爲老生代

舊空間中的分配速度很快,可是收集費用很高,所以不多執行。

node 垃圾回收

Why is garbage collection expensive?

The V8 JavaScript engine employs a stop-the-world garbage collector mechanism.

In practice, it means that the program stops execution while garbage collection is in progress.

一般,約20%的年輕一代能夠存活到老一代,舊空間的收集工做將在耗盡後纔開始。

爲此,V8 引擎使用兩種不一樣的收集算法

  1. Scavenge: 速度很快,可在新生代上運行,
  2. Mark-Sweep: 速度較慢,而且能夠在老生代上運行。

篇幅有限,關於v8垃圾回收的更多信息,能夠參考以下文章:

  1. http://jayconrod.com/posts/55...
  2. https://juejin.cn/post/684490...
  3. https://juejin.cn/post/684490...

總結

小小總結一下,上文介紹了兩種方式:

  1. 直接加大內存,使用: node --max-old-space-size=4096
  2. 把一些耗內存進程獨立出去, 使用了一個插件: fork-ts-checker-webpack-plugin

但願你們留個印象, 記得這兩種方式。

好了, 內容就這麼多, 謝謝。

才疏學淺,若有錯誤, 歡迎指正。

謝謝。

相關資料

  1. https://www.cloudbees.com/blo...
  2. http://jayconrod.com/posts/55...
  3. https://blog.risingstack.com/...

最後

若是以爲內容有幫助, 能夠關注下個人公衆號,掌握最新動態,一塊兒學習!

image.png

相關文章
相關標籤/搜索