高メモリ環境でJavaScript heap out of memory Errorを解決するためのガイド
はじめに
以前、JavaScript heap out of memory Errorが発生した場合の解消するステップについて記事にしました。
今回の記事ではその続きでメモリが不足していないにも関わらず、npm run build
を実施してJavaScript heap out of memory
が発生するパターンにおける原因とその解消方法について調べてみたので共有します。
環境
- Node: v16.0.0
- npm: v7.10.0
- Amazon EC2
Error発生時にビルドプロセスの監視
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0xa24ed0 node::Abort() [node]
2: 0x966115 node::FatalError(char const*, char const*) [node]
3: 0xb9acde v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xb9b057 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xd56ea5 [node]
6: 0xd57a2f [node]
7: 0xd65abb v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
8: 0xd6967c v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
9: 0xd2ee1d v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [node]
上記エラーについて改めて説明すると、ヒープ領域が不足してしまい、新規でメモリを確保できず、メモリ不足に陥っていることを示しています。V8 Engineはスタック領域とヒープ領域の二つののメモリ領域を持っていて、オブジェクトのような動的なデータに対しては、ヒープ領域に割り当てがされます。オブジェクトを新たに生成したけれども、ヒープ領域のメモリに空きがない状態です。
このようなエラーが発生した際、まずはwatch
コマンドを使用して、ビルドプロセスを監視します。
watch -n 1 free -h
total used free shared buff/cache available
Mem: 31G 1G 9G 1G 11G 20G
Swap: 2.0G 0B 2.0G
結果として、availableのメモリが20Gあるため、メモリ自体は十分に余裕があり、heap out of memory
は発生する余地がないように思われます。
buff/cache
に 11G 使用されていますが、これはすぐに解放可能なメモリであり、swapが使用されていない(2.0G中 0B 使用)ことから、物理メモリで十分に対応できていることが分かります。
freeのコマンド結果の意味について
- buffers: バッファキャッシュのメモリサイズ(バッファキャッシュとはファイルシステム経由ではなく、デバイスファイル経由でディスクにアクセスするときに利用されるキャッシュのこと)
- cache: ページキャッシュのメモリサイズ (ページキャッシュとはファイルシステム経由でディスクにアクセスするときに利用されるキャッシュのこと)
- available: 実質的な空きメモリ。freeが少なくなってきたら解放できるカーネル用のメモリのサイズを足したもの。
- buff/cacheは一部は解放可能だが、一部は解放不可。そのため厳密にはavailable=free + buff/cache(解放可能部分)となる。
Linuxでは, 各プロセスにメモリを割り振った残りを バッファ(buffer)とキャッシュ(cache)に利用して, ディスク入出力の負荷を減らしており、実質的な残りメモリ-は、「free + buffers + cached」で計算可能。
メモリが十分にあるのにJavaScript heap out of memory
エラーが発生する場合、以下の二つの理由が考えられます。
-
ヒープサイズの制限: デフォルトのヒープサイズ制限が不足している可能性。
これは、--max-old-space-size
オプションで調整します。 -
メモリリーク: アプリケーション内でメモリリークが発生している可能性。
ビルドプロセス中に一時的にメモリが大量に必要になることがあり、一時的なメモリ不足に陥っている可能性があります。この場合、さらにヒープサイズを増やすか、ビルド設定を見直す必要があります。
ヒープサイズの制限について
V8エンジンはデフォルトのヒープメモリ制限を保持しており、システム側で設定していなければ32bit環境では700MiB(約0.7GB), 64bit環境では1400MiB(約1.4GB)が設定されています。
その為、まずはnodeのメモリ上限を確認します。
node -e 'console.log(Math.floor(v8.getHeapStatistics().heap_size_limit/1024/1024))'
2096 # 出力結果
※ -e
オプションは、コマンドラインでスクリプトを直接実行するために使用します。
ヒープメモリ制限を確認後、必要に応じてこの値を調整します。
node --max-old-space-size=6144 $(which npm) run build # 6G
ただ上記コマンドの場合、nodeに引き渡したoptionがnpmまで渡っていないという記事を発見し、--max-old-space-size
を設定する場合は、NODE_OPTIONS
で指定した方が良いかもしれません。
NODE_OPTIONS="--max-old-space-size=4096" npm run build
Unix系システム(Linux, macOS)の場合、.bashrc
や.zshrc
などのシェル設定ファイルにメモリ上限の設定を追加することもできます。
export NODE_OPTIONS="--max-old-space-size=4096"
メモリリークについて
メモリリークとは、確保したメモリ領域を解放する処理が上手く動作しておらず、メモリ不足になる(使用していないメモリを開放することなく確保し続けてしまう)現象のことを指します。この現象が発生すると、使用されなくなったメモリ領域が適切に解放されず、プログラムが利用できるメモリの容量が減り、実行ができなくなります。
メモリリークが起きているかどうかを確認するためにprocess.memoryUsage().heapUsed
を呼び出すことによって、プロセスで使われているメモリ量をチェックすることが可能です。
node -e 'console.log(process.memoryUsage())'
node -e 'console.log(process.memoryUsage().heapUsed)'
※ -e
オプションは、Node.js コマンドラインインターフェイス(CLI)でスクリプトを直接実行するために使用
出力結果は以下の通りとなります。
{
rss: 26214400,
heapTotal: 19496960,
heapUsed: 10240320,
external: 123456
}
- rss (Resident Set Size): プロセスが使用している全メモリ量(コード、スタック、ヒープなど全てを含む)。
- heapTotal: V8エンジンが管理するヒープの総メモリ量。
- heapUsed: 実際に使用されているヒープメモリ量。
- external: V8エンジンが管理していない、C++オブジェクトなどのメモリ量。
watch
コマンドを使用しても、全メモリ量を定期的に監視することも可能です。
watch -n 1 "node -e 'console.log(process.memoryUsage())'"
出力ファイルを設定する
メモリリークが起きているかどうかを可視化してみたいので、スクリプトを作成してみます。
#!/bin/bash
# 出力ファイルの設定
output_file="memory_usage.log"
# 初期化: ファイルが存在する場合は削除
if [ -f "$output_file" ]; then
rm "$output_file"
fi
# 定期実行ループ
while true
do
# メモリ使用量を取得してファイルに追記
echo "$(date +%s),$(node -e 'console.log(process.memoryUsage().heapUsed)')" >> "$output_file"
# 1秒待つ
sleep 1
done
スクリプトに実行権限を与えて実行します。
chmod +x memory_watch.sh
./memory_watch.sh #実行
ある程度ChatGTPにお願いしつつ、Pythonでプロットしていきます。
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
import pytz
# ログファイルを読み込む
log_file = "memory_usage.log"
data = pd.read_csv(log_file, header=None, names=["timestamp", "heapUsed"])
# タイムスタンプを変換し、JSTに設定
data["timestamp"] = pd.to_datetime(data["timestamp"], unit='s')
jst = pytz.timezone('Asia/Tokyo')
data["timestamp"] = data["timestamp"].dt.tz_localize('UTC').dt.tz_convert(jst)
# グラフを作成
plt.figure(figsize=(10, 5))
plt.plot(data["timestamp"], data["heapUsed"], label="Heap Used (bytes)")
# グラフの装飾
plt.xlabel("Time (JST)")
plt.ylabel("Heap Used (bytes)")
plt.title("Memory Usage Over Time (JST)")
plt.legend()
plt.grid(True)
# グラフを表示
plt.show()
必要なパッケージをインストールして実行します。
python3 -m pip install pandas matplotlib pytz
python3 plot_memory_usage.py
出力結果から、メモリの使用量が増大していることがわかります。
メモリリークが起きていた場合は、--expose-gc
オプションを使用して手動でガベージコレクションをトリガーすることで解消します。
node --expose-gc --max-old-space-size=4096 $(which npm) run build
最後に
npm run build
時のJavaScript heap out of memory Error
の発生原因について別の角度から探ってみました。
今回記載したコードはGithubに格納しています。最後まで読んでいただきありがとうございました。
参考文献
アルサーガパートナーズ株式会社のエンジニアによるテックブログです。11月14(木)20時〜 エンジニア向けセミナーを開催!詳細とご応募は👉️ arsaga.jp/news/pressrelease-cheer-up-project-november-20241114/
Discussion