Java アプリケーションでのメモリ問題のトラブルシューティング
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2024年01月18日(木)
Table of Contents
アプリケーションのメモリ使用量を調整するには、Java がメモリをどのように使用するのか、およびアプリケーションのメモリ使用をどのように可視化できるかの両方を理解する必要があります。
JVM メモリ使用量
JVM はさまざまな方法でメモリを使用します。メモリは主にヒープとして使用されますが、これだけではありません。メモリはヒープ以外に、Metaspaceやスタックによっても使用されます。
Java ヒープ - ヒープとは、クラスのインスタンス (あるいはオブジェクト) が保管される場所です。インスタンス変数はオブジェクトに保管されます。Java のメモリおよび最適化について議論するとき、ほとんどの場合はヒープについての議論となります。これは、ヒープはユーザーによってほぼ制御でき、この場所でガベージコレクション (および GC 最適化) が実行されるためです。ヒープサイズは -Xms
および -Xmx
の JVM フラグで制御されます。詳しくは、GC and The Heap (GC およびヒープ) を参照してください。
Java スタック - 各スレッドには独自のコールスタックがあります。スタックにはプリミティブ型のローカル変数とオブジェクト参照のほか、コールスタック (メソッド呼び出し) 自体も格納されています。スタックはスタックフレームがコンテキストを離れるとクリーンアップされるため、ここでは GC は実行されません。-Xss
JVM オプションは、スレッドのスタックごとに割り当てられるメモリの量を制御します。
Metaspace - Metaspace は、オブジェクトのクラス定義を保管します。Metaspace のサイズは -XX:MetaspaceSize
を設定して制御します。
追加の JVM オーバーヘッド - 上記に加えて、一部のメモリが JVM 自体によって消費されます。これは JVM 用の C ライブラリと、残りのメモリプールを実行するために使用する一部の C メモリ割り当てオーバーヘッドを保持します。JVM 上で実行する可視性ツールはこのオーバーヘッドを表示しないため、アプリケーションがメモリを使用する方法についての概念を示すことはできますが、JVM プロセッサの合計メモリ使用量を表示することはできません。この種類のメモリは、glibc メモリ動作の調整による影響を受けることがあります。
コンテナ内で実行するための Java の設定
JVM は、使用可能なオペレーティングシステムレポートに基づき、さまざまなメモリカテゴリのデフォルト割り当て値を設定しようとします。ただし、コンテナ (Heroku の dyno や Docker コンテナなど) の内部で実行するとき、OS によって報告される値が正しくないことがあります。これは、cgroup メモリ制限を代わりに使用するよう JVM を設定することによって回避できます。
Java 8 では、cgroup メモリ制限の使用は試験的な機能であり、次のオプションを (Procfile
内または環境変数のいずれかで) JVM プロセスに追加することによって有効化できます。
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
Java 9 以降では、このオプションは試験的なものではなくなりました。
-XX:+UseContainerSupport
これらのオプションは JVM プロセスの全体的なメモリフットプリントを減少させることがあります。そうならない場合、別の調整を行う前に、JVM がメモリを使用する方法を調査する必要があります。
Java アプリケーションのメモリ使用のプロファイリング
アプリケーションが開発環境と本番環境の両方でメモリを使用する方法を理解することは重要です。メモリの問題の大半は、あまり手間をかけずにあらゆる環境で再現できます。通常はメモリの問題をローカルマシンでトラブルシューティングする方が簡単です。これは、多くのツールにアクセスでき、監視ツールが引き起こす可能性がある副作用について心配しなくて済むためです。
Java アプリケーションのメモリ使用について洞察を得るために使用できるツールはいくつか存在します。一部のツールは Java ランタイム自体にパッケージ化されており、開発マシンにすでに存在しています。サードパーティから入手できるものもあります。ここでは網羅的なリストを示すつもりはなく、これらのツールを検討する上での出発点を示しているにすぎません。
Java ランタイムに付属するツールとしては、jmap
(ヒープダンプの実行とメモリ統計の収集)、jstack
(あらゆる時点での実行中のスレッドの検査)、jstat
(一般的な JVM 統計の収集)、および jhat
(ヒープダンプの分析) などがあります。これらのツールの詳細については、Oracle Docs または IBM developer works を参照してください。
Heroku アプリケーション関連の指標は、ヒープおよびヒープ以外のメモリおよび GC アクティビティのグラフを提供します。この機能を使用可能にするための詳しい説明については、言語ランタイム関連の指標のドキュメントを参照してください。
VisualVM は、上記のすべてのツールを、一部のユーザーにとって使いやすい GUI ベースのパッケージに組み合わせたものです。
YourKit は優れた市販のツールです。
Heroku のメモリ制限
アプリケーションで使用できる物理メモリの量は、dyno タイプによって異なります。アプリケーションではこれよりも多くのメモリを消費することができますが、dyno はディスクへのページングを開始します。これによってパフォーマンスが大きく損なわれるため、避けることが賢明です。このページングが発生し始めると、アプリケーションログに R14 エラーが表示されます。
JVM ベースのほとんどの言語に対するデフォルトのサポートでは、dyno タイプに基づき、-Xss512k
および Xmx
を動的に設定します。これらのデフォルトにより、ほとんどのアプリケーションは R14 エラーを回避できます。デフォルトの完全なセットについては、Language Support Docs (言語サポートドキュメント) で、選択した言語およびフレームワークを参照してください。
Heroku での Java アプリケーションのプロファイリング
上記のプロファイリングツールの使用は Heroku のクラウド上では異なりますが、これはプラットフォームのプロセス分離モデルが原因です。ただし、Heroku の JVM 言語サポートでは、これらのツールを Heroku に接続する方法を簡素化するツールが提供されています。
このセクションに記載されているツールは、Heroku CLI 用の heroku-cli-java
プラグインが必要です。次のようにしてインストールします。
$ heroku plugins:install heroku-cli-java
dyno へのプロファイリングツールの接続
Heroku Exec を使用して、多くの Java プロファイリングツールおよびデバッグツールを、Web dyno 内で実行中の JVM プロセスにアタッチできます。機能が有効化されると、スレッドダンプおよびヒープダンプを取得したり、JConsole や VisualVM などの一般的な GUI ベースのツールをアタッチしたりできます。たとえば、次のコードを実行できます。
$ heroku java:visualvm
これにより、web.1
dyno に接続された VisualVM セッションが開始します。オプションとして、--dyno
フラグを使用して、接続先の dyno を指定できます。
スレッドダンプの生成
Heroku Exec を有効化すると、dyno で実行中のアプリケーションプロセスのスレッドダンプを、次のコマンドを使用して生成できます。
$ heroku java:jstack
これにより、web.1
dyno からのスレッドダンプがコンソールに出力されます。オプションで、ダンプの生成元となる dyno の名前を付けて --dyno
フラグを指定することができます。
Heroku では jstack
で -F
オプションを使用することはできません。
ヒープダンプの生成
Heroku Exec を有効化すると、dyno で実行中のアプリケーションプロセスのヒープダンプを、次のコマンドを使用して生成できます。
$ heroku java:jmap
これにより、ヒープのヒストグラムが web.1
dyno からコンソールに出力されます。オプションで、ダンプの生成元となる dyno の名前を付けた --dyno
フラグを heroku java:jmap
コマンドに指定することができます。HPROF 形式のバイナリヒープダンプが必要な場合、次のコマンドを実行できます。
$ heroku java:jmap --hprof
バイナリファイルは VisualVM、jhat
、Eclipse MAT などのツールを使用して後で分析できます。
高度な jmap
オプションを使用する必要がある場合は、heroku ps:exec
を実行して dyno へのシェルセッションを開始し、そこで jmap
を実行します。バイナリヒープダンプを生成した場合、heroku ps:copy
を実行することによって dyno の外部にこれをコピーできます。
Heroku では jmap
で -F
オプションを使用することはできません。
JRuby の場合は、heroku buildpacks:add -i 1 heroku/jvm
を実行することによって、jvm-common を buildpack に明示的に追加する必要があります。
NativeMemoryTracking の設定
アプリのネイティブメモリ使用量が高レベルにある場合 (RSS の合計と JVM ヒープとの差など)、シャットダウン時にネイティブメモリ追跡情報を出力するようアプリケーションを設定することが必要な場合もあります。これを行うには、次の環境変数を設定します。
$ heroku config:set JAVA_OPTS="-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics"
この設定により、次のコマンドを実行して One-off dyno 開始することによって、分離された環境内でメモリ使用をデバッグすることもできます。
$ heroku run bash
その後、Procfile
コマンドの末尾に &
を追加することによって、アプリプロセスをバックグラウンドで実行します。たとえば、次のようになります。
$ java -jar myapp.jar &
分離プロセスではなくライブプロセスにアタッチする場合、Heroku Exec を使用して、heroku ps:exec
を実行することにより、実行中の Wed dyno を検査できます。
いずれの場合も、次のコマンドを実行することによって、Java プロセスのプロセス ID (PID) を取得できます。
$ jps
4 Main
105 Jps
この例では、PID は 4 です。ここで、jstack
、jmap
、jcmd
などのツールをこのプロセスに対して使用することができます。たとえば、次のようになります。
$ jcmd 4 VM.native_memory summary
4:
Native Memory Tracking:
Total: reserved=1811283KB, committed=543735KB
- Java Heap (reserved=393216KB, committed=390656KB)
(mmap: reserved=393216KB, committed=390656KB)
- Class (reserved=1095741KB, committed=54165KB)
(classes #8590)
(malloc=10301KB #14097)
(mmap: reserved=1085440KB, committed=43864KB)
- Thread (reserved=22290KB, committed=22290KB)
(thread #30)
(stack: reserved=22132KB, committed=22132KB)
(malloc=92KB #155)
(arena=66KB #58)
...
jcmd
を使用してネイティブメモリをデバッグすることの詳細については、ネイティブメモリトラッキングに関する Oracle のドキュメントを参照してください。
アプリからのスレッドダンプの生成
アプリケーションコードからスレッドダンプを生成することもできます。これはプロセスの終了時にダンプの生成を試行するときに便利です。たとえば、次のようなコードを Java アプリケーションに追加することもできます。
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
final java.lang.management.ThreadMXBean threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean();
final java.lang.management.ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100);
for (java.lang.management.ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadName());
final Thread.State state = threadInfo.getThreadState();
System.out.println(" java.lang.Thread.State: " + state);
final StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
for (final StackTraceElement stackTraceElement : stackTraceElements) {
System.out.println(" at " + stackTraceElement);
}
System.out.println("\n");
}
}
});
あるいは Scala Play アプリケーションで、次の内容を持つ app/Global.scala
ファイルを追加することができます。
object Global extends WithFilters() {
override def onStop(app: Application) {
var threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean();
var threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds, 100);
threadInfos.foreach { threadInfo =>
if (threadInfo != null) {
println(s"""
'${threadInfo.getThreadName}': ${threadInfo.getThreadState}
at ${threadInfo.getStackTrace.mkString("\n at ")}
""")
}
}
}
}
これらの例は両方とも、プロセスがシャットダウンするときにスレッド情報を stdout に出力します。このようにして、プロセスがデッドロックに陥った場合、次のようなコマンドでプロセスを再起動できます。
$ heroku ps:restart web.1
そしてスタック情報がログに表示されます。
冗長な GC フラグ
上記の情報が十分に詳細でない場合、GC 実行時に冗長な出力をログに取得するために使用できる JVM オプションもいくつか存在します。次のフラグを Java opts に追加します。-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps
$ heroku config:set JAVA_OPTS='-Xss512k -XX:+UseCompressedOops -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps'
2012-07-07T04:27:59+00:00 app[web.2]: {Heap before GC invocations=43 (full 0):
2012-07-07T04:27:59+00:00 app[web.2]: PSYoungGen total 192768K, used 190896K [0x00000000f4000000, 0x0000000100000000, 0x0000000100000000)
2012-07-07T04:27:59+00:00 app[web.2]: eden space 188800K, 100% used [0x00000000f4000000,0x00000000ff860000,0x00000000ff860000)
2012-07-07T04:27:59+00:00 app[web.2]: from space 3968K, 52% used [0x00000000ffc20000,0x00000000ffe2c1e0,0x0000000100000000)
2012-07-07T04:27:59+00:00 app[web.2]: to space 3840K, 0% used [0x00000000ff860000,0x00000000ff860000,0x00000000ffc20000)
2012-07-07T04:27:59+00:00 app[web.2]: ParOldGen total 196608K, used 13900K [0x00000000e8000000, 0x00000000f4000000, 0x00000000f4000000)
2012-07-07T04:27:59+00:00 app[web.2]: object space 196608K, 7% used [0x00000000e8000000,0x00000000e8d93070,0x00000000f4000000)
2012-07-07T04:27:59+00:00 app[web.2]: PSPermGen total 50816K, used 50735K [0x00000000dda00000, 0x00000000e0ba0000, 0x00000000e8000000)
2012-07-07T04:27:59+00:00 app[web.2]: object space 50816K, 99% used [0x00000000dda00000,0x00000000e0b8bee0,0x00000000e0ba0000)
2012-07-07T04:27:59+00:00 app[web.2]: 2012-07-07T04:27:59.361+0000: [GC
2012-07-07T04:27:59+00:00 app[web.2]: Desired survivor size 3866624 bytes, new threshold 1 (max 15)
2012-07-07T04:27:59+00:00 app[web.2]: [PSYoungGen: 190896K->2336K(192640K)] 204796K->16417K(389248K), 0.0058230 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2012-07-07T04:27:59+00:00 app[web.2]: Heap after GC invocations=43 (full 0):
2012-07-07T04:27:59+00:00 app[web.2]: PSYoungGen total 192640K, used 2336K [0x00000000f4000000, 0x0000000100000000, 0x0000000100000000)
2012-07-07T04:27:59+00:00 app[web.2]: eden space 188800K, 0% used [0x00000000f4000000,0x00000000f4000000,0x00000000ff860000)
2012-07-07T04:27:59+00:00 app[web.2]: from space 3840K, 60% used [0x00000000ff860000,0x00000000ffaa82d0,0x00000000ffc20000)
2012-07-07T04:27:59+00:00 app[web.2]: to space 3776K, 0% used [0x00000000ffc50000,0x00000000ffc50000,0x0000000100000000)
2012-07-07T04:27:59+00:00 app[web.2]: ParOldGen total 196608K, used 14080K [0x00000000e8000000, 0x00000000f4000000, 0x00000000f4000000)
2012-07-07T04:27:59+00:00 app[web.2]: object space 196608K, 7% used [0x00000000e8000000,0x00000000e8dc0330,0x00000000f4000000)
2012-07-07T04:27:59+00:00 app[web.2]: PSPermGen total 50816K, used 50735K [0x00000000dda00000, 0x00000000e0ba0000, 0x00000000e8000000)
2012-07-07T04:27:59+00:00 app[web.2]: object space 50816K, 99% used [0x00000000dda00000,0x00000000e0b8bee0,0x00000000e0ba0000)
2012-07-07T04:27:59+00:00 app[web.2]: }
Heroku Labs: log-runtime-metrics
Heroku Labs 機能に log-runtime-metrics と呼ばれるものがあります。これは合計メモリ使用量などの診断情報をアプリケーションログに出力します。
New Relic
一部の JVM 言語および Java フレームワークでは、New Relic Java エージェントを使用できます。
Heroku 上で実行するためのメモリのヒント
- スレッドの使用とスタックサイズに注意してください。デフォルトオプション
-Xss512k
は、各スレッドが 512 KB のメモリを使用することを意味します。このオプションがない JVM のデフォルトは 1 MB です。 - 重量級の監視エージェントに注意してください。一部の Java エージェントはそれ自体がメモリを著しく使用することがあり、問題のトラブルシューティングを試行するときにメモリの問題がさらに悪化することがあります。メモリの問題がある場合、エージェントを削除することが最初の一歩として適切です。上記のメモリロギングエージェントはメモリフットプリントが非常に小さいため、これらの問題を発生させません。
それでもなおメモリの問題が存在する場合は、Heroku Supportにいつでもご連絡ください。