🦘

高メモリ環境でJavaScript heap out of memory Errorを解決するためのガイド

2024/07/30に公開

はじめに

以前、JavaScript heap out of memory Errorが発生した場合の解消するステップについて記事にしました。

https://zenn.dev/arsaga/articles/291ff290543996


今回の記事ではその続きでメモリが不足していないにも関わらず、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」で計算可能。

https://blog.framinal.life/entry/2020/04/23/080124

http://www.math.kobe-u.ac.jp/~kodama/tips-free-memory.html


メモリが十分にあるのにJavaScript heap out of memoryエラーが発生する場合、以下の二つの理由が考えられます。

  1. ヒープサイズの制限: デフォルトのヒープサイズ制限が不足している可能性。
    これは、--max-old-space-sizeオプションで調整します。

  2. メモリリーク: アプリケーション内でメモリリークが発生している可能性。 
    ビルドプロセス中に一時的にメモリが大量に必要になることがあり、一時的なメモリ不足に陥っている可能性があります。この場合、さらにヒープサイズを増やすか、ビルド設定を見直す必要があります。



ヒープサイズの制限について

V8エンジンはデフォルトのヒープメモリ制限を保持しており、システム側で設定していなければ32bit環境では700MiB(約0.7GB), 64bit環境では1400MiB(約1.4GB)が設定されています。

https://zenn.dev/legalscape/articles/a0715a699eacb5


その為、まずはnodeのメモリ上限を確認します。

node -e 'console.log(Math.floor(v8.getHeapStatistics().heap_size_limit/1024/1024))'
2096 # 出力結果

-eオプションは、コマンドラインでスクリプトを直接実行するために使用します。

https://nodejs.org/api/v8.html#v8getheapstatistics


ヒープメモリ制限を確認後、必要に応じてこの値を調整します。

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

https://www.gaji.jp/blog/2019/12/17/1860/


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++オブジェクトなどのメモリ量。

https://nodejs.org/api/process.html#processmemoryusage

watchコマンドを使用しても、全メモリ量を定期的に監視することも可能です。

watch -n 1 "node -e 'console.log(process.memoryUsage())'"



出力ファイルを設定する

メモリリークが起きているかどうかを可視化してみたいので、スクリプトを作成してみます。

memory_watch.sh
#!/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

https://www.kushiro-ct.ac.jp/yanagawa/ex-2017/3-unix/03.html


スクリプトに実行権限を与えて実行します。

chmod +x memory_watch.sh
./memory_watch.sh #実行



ある程度ChatGTPにお願いしつつ、Pythonでプロットしていきます。

plot_memory_usage.py
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

https://postd.cc/simple-guide-to-finding-a-javascript-memory-leak-in-node-js/#min-theory

https://trueman-developer.blogspot.com/2018/03/nodejs.html

https://www.yoheim.net/blog.php?q=20170205



最後に

npm run build時のJavaScript heap out of memory Errorの発生原因について別の角度から探ってみました。

今回記載したコードはGithubに格納しています。最後まで読んでいただきありがとうございました。

https://github.com/MASAKi-cell/plot_memory



参考文献

https://tech-blog.lakeel.com/n/na84923863e2a

https://zenn.dev/ubie_dev/articles/f64561d59918d1

http://www.math.kobe-u.ac.jp/~kodama/tips-free-memory.html

Arsaga Developers Blog

Discussion