java.lang.OutOfMemoryError: MetaspaceをJavaアプリケーションで修正する方法

intermediate Java2026-04-08| Java 8以降、JVM(HotSpot)、Linux/macOS/Windows、Spring Boot、Tomcat、長期稼働Javaアプリ全般

Error Message

java.lang.OutOfMemoryError: Metaspace
#java#jvm#metaspace#メモリ#クラスローダー

エラーの内容

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)

このエラーが起動時に現れることはほとんどありません。多くの場合、アプリは数時間、時には数日間正常に動作した後にクラッシュします。HibernateやSpringのWrappedExceptionの中に埋もれていることもあり、気づきにくいケースもあります。

Metaspaceとは何か

Java 8ではPermGenがMetaspaceに置き換えられました。Metaspaceはクラスのメタデータ、つまりロードされたすべてのクラスのJVM内部表現を格納します。ヒープとは異なり、ネイティブメモリ上に存在します。デフォルトでは上限がありませんが、システムがメモリ不足の状態にある場合や-XX:MaxMetaspaceSizeを設定している場合は、利用可能なメモリを使い果たす可能性があります。

主な原因は2つです:

  • Metaspaceの上限が小さすぎる-XX:MaxMetaspaceSizeをアプリが実際に必要とするクラスのフットプリントより低く設定している場合。
  • クラスローダーのリーク — クラスがロードされ続けるがアンロードされない状態。動的プロキシ、スクリプトエンジン、ホットリロードのコードがよくある原因です。

ステップ1 — 現在のMetaspace使用量を確認する

フラグを変更する前に、ベースラインを取得しましょう:

# アプリ実行中に:
jcmd <PID> VM.native_memory summary

# またはjstatで(ロードされたクラス数):
jstat -class <PID> 1000 10

# クラスローダーごとのスナップショット:
jmap -clstats <PID> | sort -k3 -rn | head -20

jcmdの出力でMetaspaceセクションを確認します。committedMaxMetaspaceSizeに近い場合は、上限を引き上げるだけで解決します。しかしjstatでアイドル時でもクラス数が増え続けている場合は、設定の問題ではなくリークです。

ステップ2 — Metaspaceの上限を引き上げる(即効策)

-XX:MaxMetaspaceSizeが低すぎる場合は最もシンプルなケースです。値を増やしましょう:

# JVM引数に追加または変更:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

MetaspaceSizeは初期しきい値を設定します — 高めに設定することで起動時の不要なGCを回避できます。MaxMetaspaceSizeはハードな上限です。ほとんどのSpring Bootアプリでは256〜512mで十分です。プラグインアーキテクチャが複雑なアプリや動的クラス生成(GroovyヘビーやOSGiなど)では768m以上が必要な場合もあります。

application.propertiesの場合(Spring Boot Mavenプラグイン経由):

spring-boot.run.jvmArguments=-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

Dockerfileの場合:

ENV JAVA_OPTS="-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
CMD java $JAVA_OPTS -jar app.jar

Tomcatのcatalina.shの場合:

export JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"

ステップ3 — クラスローダーリークを診断する

上限を引き上げても、Metaspaceがまだ増え続けている場合はリークがあります。ヒープダンプの分析が最も確実な方法です。

# ヒープダンプを取得:
jmap -dump:format=b,file=heap.hprof <PID>

# またはOOM発生時に自動ダンプするようJVMを設定:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof

ダンプをEclipse MATまたはVisualVMで開きます。Class Loader Explorerに移動します。同じクラスローダーのインスタンスが500個表示されている場合、それがリークの原因です。

よくある原因:

  • リクエストごとに新しいClassLoaderを起動するライブラリ(一部のXMLパーサー、スクリプトエンジン)
  • サイズ制限のないGroovy/BeanShell/Velocityスクリプトキャッシュ
  • 本番環境で動作しているSpring DevToolsのホットリロード(本番には含めないこと)
  • キャッシュなしでループ内にプロキシクラスを生成するCGLIBやJavassist
  • デプロイ時に登録されたがアンデプロイ時に登録解除されないJDBCドライバ

ステップ4 — リークを修正する

CGLIB / 動的プロキシ

Enhancer.create()を呼び出すたびに新しいクラスが生成されます。リクエストごとに実行するとMetaspaceがあふれます。プロキシをキャッシュしましょう:

// BAD — 呼び出しのたびに新しいクラスを生成
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
MyService proxy = (MyService) enhancer.create();

// GOOD — プロキシクラスをキャッシュ
private static final MyService PROXY = createProxy();
private static MyService createProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyService.class);
    return (MyService) enhancer.create();
}

Groovy Script Engine

評価のたびに新しいGroovyShellを作成するのは典型的なリークです。シェルを再利用してコンパイル済みスクリプトをキャッシュしましょう:

// BAD
new GroovyShell().evaluate(script);

// BETTER — シェルを再利用してコンパイル済みスクリプトをキャッシュ
private final GroovyShell shell = new GroovyShell();
private final Map<String, Script> cache = new ConcurrentHashMap<>();

public Object eval(String src) {
    return cache.computeIfAbsent(src, shell::parse).run();
}

TomcatアンデプロイのJDBCドライバ

WebアプリのServletContextListenerにクリーンアップロジックを追加します:

@Override
public void contextDestroyed(ServletContextEvent sce) {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        try {
            DriverManager.deregisterDriver(driver);
        } catch (SQLException e) {
            log.warn("Failed to deregister driver", e);
        }
    }
}

ステップ5 — 修正を確認する

変更を適用した後、Metaspaceを継続的に監視します:

# 2分間、5秒ごとにクラス数を監視:
jstat -class <PID> 5000 24

# またはGCログを有効化してMetaspaceを確認:
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=20m
grep -i metaspace /tmp/gc.log | tail -20

正常なアプリは起動後にクラス数が安定して横ばいになります。通常の操作中に1分あたり100以上の新しいクラスが発生し続けている場合は、リークがまだ修正されていません。

上限引き上げの修正の場合:アプリをいくつかの完全なリクエストサイクルで実行し、Metaspaceの使用量が新しい上限を大幅に下回る水準で安定していることを確認します。それでも上昇傾向にある場合は、両方の問題が存在しています。

JVMフラグのまとめ

# Metaspaceの初期値と最大値を設定
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# OOM発生時にヒープダンプを出力(事後分析用)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/myapp/oom.hprof

# ネイティブメモリトラッキングを有効化(軽量)
-XX:NativeMemoryTracking=summary

# クラスメタデータを回収するためGCをより積極的に実行
-XX:+CMSClassUnloadingEnabled   # Java 8 CMS専用
-XX:+ClassUnloadingWithConcurrentMark  # G1/ZGC

クイックチェックリスト

  • MaxMetaspaceSizeが低すぎないか確認 — まず引き上げる
  • jstat -classでクラス数を監視 — 横ばい=正常、増加し続ける=リーク
  • ヒープダンプ + MAT Class Loader Explorerでリーク箇所を特定
  • 動的プロキシやスクリプトエンジンの誤った使用を確認
  • Webアプリのシャットダウン時にJDBCドライバを登録解除する
  • 本番環境でSpring DevToolsを絶対に使用しない

Related Error Notes