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

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

提示已經(jīng)很明顯:Javascript Heap out of memory.
看到內(nèi)存溢出這個(gè)關(guān)鍵字,我們一般都會考慮到是因?yàn)?Node.js 內(nèi)存不夠?qū)е碌摹?/p>
但 Node 進(jìn)程的內(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.
翻譯一下:
當(dāng)前,默認(rèn)情況下,V8在32位系統(tǒng)上的內(nèi)存限制為512mb,在64位系統(tǒng)上的內(nèi)存限制為1gb。
可以通過將
--max-old-space-size設(shè)置為最大?1gb(32位)和?1.7gb(64位)來提高此限制,但是如果達(dá)到內(nèi)存限制, 建議您將單個(gè)進(jìn)程拆分為多個(gè)工作進(jìn)程。
如果你想知道自己電腦的內(nèi)存限制有多大, 可以直接把內(nèi)存撐爆, 看報(bào)錯(cuò)。
運(yùn)行如下代碼:
// 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();
不出意外, 你將喜提如下報(bào)錯(cuò):

我的電腦是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,這段代碼大概在 1.6 GB 左右內(nèi)存時(shí)候拋出異常。
那我們現(xiàn)在知道 Node Process 確實(shí)是有一個(gè)內(nèi)存限制的, 那我們就來增大它的內(nèi)存限制再試一下。
用 node --max-old-space-size=6000 來運(yùn)行這段代碼,得到如下結(jié)果:

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