項目構(gòu)建內(nèi)存溢出了?看看 Node 內(nèi)存限制

背景
在之前的一篇文章中, 我們遇到了一個項目在構(gòu)建時內(nèi)存溢出的問題。
當時的解決方案是: 直接調(diào)大 node 的內(nèi)存限制,避免達到內(nèi)存上限。
今天聽同事分享了一個新方法,覺得不錯, 特此記錄, 順便分享給大家。
正文
報錯示意圖:

提示已經(jīng)很明顯:Javascript Heap out of memory.
看到內(nèi)存溢出這個關(guān)鍵字,我們一般都會考慮到是因為 Node.js 內(nèi)存不夠?qū)е碌摹?/p>
但 Node 進程的內(nèi)存限制會是多少呢?
在網(wǎng)上查閱了到如下描述:
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.
翻譯一下:
當前,默認情況下,V8在32位系統(tǒng)上的內(nèi)存限制為512mb,在64位系統(tǒng)上的內(nèi)存限制為1gb。
可以通過將
--max-old-space-size設(shè)置為最大?1gb(32位)和?1.7gb(64位)來提高此限制,但是如果達到內(nèi)存限制, 建議您將單個進程拆分為多個工作進程。
如果你想知道自己電腦的內(nèi)存限制有多大, 可以直接把內(nèi)存撐爆, 看報錯。
運行如下代碼:
// 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 左右內(nèi)存時候拋出異常。
那我們現(xiàn)在知道 Node Process 確實是有一個內(nèi)存限制的, 那我們就來增大它的內(nèi)存限制再試一下。
用 node --max-old-space-size=6000 來運行這段代碼,得到如下結(jié)果:

內(nèi)存達到 4.6G 的時候也溢出了。
你可能會問, node 不是有內(nèi)存回收嗎?這個我們在下面會講。
使用這個參數(shù):node --max-old-space-size=6000, 我們增加的內(nèi)存中老生代區(qū)域的大小,比較暴力。
就像上文中提到的:如果達到內(nèi)存限制, 建議您將單個進程拆分為多個工作進程。
這個項目是一個 ts 項目,ts 文件的編譯是比較占用內(nèi)存的,如果把這部分獨立成一個單獨的進程, 情況也會有所改善。
因為 ts-loader 內(nèi)部調(diào)用了 tsc,在使用 ts-loader 時,會使用 tsconfig.js配置文件。
當項目中的代碼變的越來越多,體積也越來越龐大時,項目編譯時間也隨之增加。
這是因為 Typescript 的語義檢查器必須在每次重建時檢查所有文件。
ts-loader 提供了一個 transpileOnly 選項,它默認為 false,我們可以把它設(shè)置為 true,這樣項目編譯時就不會進行類型檢查,也不會輸出聲明文件。
對一下 transpileOnly 分別設(shè)置 false 和 true 的項目構(gòu)建速度對比:
當 transpileOnly 為 false 時,整體構(gòu)建時間為 4.88s. 當 transpileOnly 為 true 時,整體構(gòu)建時間為 2.40s.
雖然構(gòu)建速度提升了,但是有了一個弊端: 打包編譯不會進行類型檢查。
好在官方推薦了這樣一個插件, 提供了這樣的能力: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));
}
修改之后, 構(gòu)建就成功了。
關(guān)于Node垃圾回收
在 Node.js 里面,V8 自動幫助我們進行垃圾回收, 讓我們簡單看一下V8中如何處理內(nèi)存。
一些定義
常駐集大?。菏荝AM中保存的進程所占用的內(nèi)存部分,其中包括: 代碼本身 棧 堆 stack:包含原始類型和對對象的引用 堆:存儲引用類型,例如對象,字符串或閉包 對象的淺層大?。簩ο蟊旧沓钟械膬?nèi)存大小 對象的保留大?。簞h除對象及其相關(guān)對象后釋放的內(nèi)存大小
垃圾收集器如何工作
垃圾回收是回收由應(yīng)用程序不再使用的對象所占用的內(nèi)存的過程。
通常,內(nèi)存分配很便宜,而內(nèi)存池用完時收集起來很昂貴。
如果無法從根節(jié)點訪問對象,則該對象是垃圾回收的候選對象,因此該對象不會被根對象或任何其他活動對象引用。
根對象可以是全局對象,DOM元素或局部變量。
堆有兩個主要組成部分,即 New Space和 Old Space。
新空間是進行新分配的地方。
在這里收集垃圾的速度很快,大小約為1-8MB。
留存在新空間中的物體被稱為新生代。
在新空間中幸存下來的物體被提升的舊空間-它們被稱為老生代。
舊空間中的分配速度很快,但是收集費用很高,因此很少執(zhí)行。
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引擎使用兩種不同的收集算法:
Scavenge: 速度很快,可在 新生代上運行,Mark-Sweep: 速度較慢,并且可以在 老生代上運行。
篇幅有限,關(guān)于v8垃圾回收的更多信息,可以參考如下文章:
http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection https://juejin.cn/post/6844903878928891911 https://juejin.cn/post/6844903859089866760
總結(jié)
小小總結(jié)一下,上文介紹了兩種方式:
直接加大內(nèi)存,使用: node --max-old-space-size=4096把一些耗內(nèi)存進程獨立出去, 使用了一個插件: fork-ts-checker-webpack-plugin
希望大家留個印象, 記得這兩種方式。
好了, 內(nèi)容就這么多, 謝謝。
相關(guān)資料
https://www.cloudbees.com/blog/understanding-garbage-collection-in-node-js/ http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection https://blog.risingstack.com/finding-a-memory-leak-in-node-js/
“分享、點贊、在看” 支持一波
