Javaメモリリークが原因の性能トラブルを分析する方法
~ jcmdツールの使い方 ~

富士通技術者ブログ~Javaミドルウェア~

2025年8月29日 初版
桐山 卓弥

Javaアプリケーションを実行していると、レスポンスが遅くなるなどの性能劣化のトラブルに直面することがあります。Javaの性能劣化の要因は多岐にわたりますが、その中の1つに「メモリリーク」があります。
メモリリーク分析の方法には、jcmdでヒープダンプを採取する方法とJDK Flight Recorder(以降、JFR)を利用する方法があります。本稿では、メモリリークの概要について説明し、2つの分析方法の特徴を紹介した後、jcmdによる分析方法を説明します。JFRを使った方法については、別記事での解説を予定しています。

本稿では、「FUJITSU Software Enterprise Application Platform V2.0」に同梱しているOpenJDK 21を例に説明します。

1. Javaのメモリリーク

開発者がメモリ管理を意識してコーディングする必要があるCやC++のような言語とは異なり、Javaはガベージコレクション (以降、GC) がJavaヒープのメモリ領域を自動的に解放・再利用します。GCは、Javaヒープに存在する不要になったオブジェクトを自動的に回収する仕組みです。これにより、開発者は複雑なメモリ管理を意識せずにアプリケーションを開発することができます。

Javaのメモリリークとは、アプリケーションによって参照を誤って保持し続けられたオブジェクトがGCによって回収されず、メモリ領域を占有し続ける状態を指します。GCはどこからも参照されなくなったオブジェクトを不要と判断して回収しますが、まだ参照されているオブジェクトは回収できません。メモリリークが放置されると、利用可能なメモリ領域が徐々に減少し、最終的にはOutOfMemoryError (以降、OOME) を引き起こすことがあります。OOMEが発生すると、アプリケーションが強制終了されるなど、システムの安定性を大きく損なうことになります。また、OOMEに至らない場合でも、GCの実行頻度が増加し、アプリケーションのレスポンスが悪化する原因となります。

メモリリークの原因を特定するには、GCの対象にならずJavaヒープに残り続けているオブジェクトの参照関係を詳細に調査することが必要です。調査方法として伝統的に用いられてきたのが、ヒープダンプの解析です。ヒープダンプは、採取時点のJavaヒープ全体のメモリ状態をスナップショットとして取得するもので、どのオブジェクトがどこから参照されていてどれだけJavaヒープに残っているか調べることができます。
しかし、ヒープダンプには採取時の負荷が大きい、時間経過による変化がわからないといった課題がありました。そこでメモリリーク分析の別の選択肢になるのが、新しいプロファイリングツールであるJFRを使用したデータ採取です。JFRはOpenJDKに同梱されており、低い負荷で継続的にデータ採取できるという、ヒープダンプにはない利点があります。
次章では、最も簡単にヒープダンプを採取できるjcmdを使う方法とJFRを使ってデータ採取する方法の特徴について説明します。

2. jcmdとJFRの特徴

表1はjcmdでヒープダンプを採取する方法とJFRを使ってデータ採取する方法の特徴を比較したものです。

表1: 分析方法の比較
方法 jcmd JFR
アプリケーションへの負荷 高い 低い
出力ファイルサイズ 大きい 小さい
記録形式 ヒープダンプ イベントと呼ばれる
JFR固有データ
記録内容 ヒープに含まれる
全オブジェクト、参照関係
一部のオブジェクト、参照関係、生成時のスタックトレース
記録タイミング ヒープダンプ採取時点のみ 継続的な記録

JFRは低負荷で継続的にデータを採取できます。性能への影響を与えたくない本番環境でデータを採取したい場合はJFRを使った方法を選択して下さい。また、メモリリークがいつ起きるか予期できず、データ採取のタイミングがわからない場合も、JFRを使って長期的にデータを記録する方法が有効です。
jcmdでヒープダンプを採取するとアプリケーションに高負荷を与えますが、ヒープダンプを解析することで、ヒープの状況を詳細に調査することができます。負荷を気にせずに、メモリリークの原因となるオブジェクトの詳細な参照関係を調べたい場合は、jcmdでヒープダンプを採取する分析方法を選択してください。

なお、JDK8以前は、ヒープダンプを採取するためにjmapが使われることもありましたが、現在はjcmdに統合されており、こちらを使うことが推奨されています。詳細は[ Enterprise Application Platformでのトラブルシューティング技法(第1回):OpenJDK 11のJDKツールの概要を知る]の記事を参照してください。

次章では、jcmdによるメモリリーク分析の手順を説明します。JFRを使う手順については、別記事での説明を予定しています。

3. jcmdによるメモリリーク分析

本章では、メモリリークを意図的に引き起こすコードを例に、jcmdを使ってメモリリーク分析する手順について説明します。
メモリリークの原因となっているオブジェクトへの参照元を特定するために、jcmdを使用してクラスヒストグラムとヒープダンプを採取して分析します。

以下は、オブジェクトへの参照を誤って保持し続けてメモリリークを意図的に引き起こすコードの例です。

 1 package com.fujitsu.demo;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class MemLeak {
 7     List objects = new ArrayList<>();
 8 
 9     public static void main(String[] args) {
10         MemLeak memLeak = new MemLeak();
11         memLeak.leak();
12     }
13 
14     private void leak() {
15         while (true) {
16             LeakingObject o = new LeakingObject();
17             objects.add(new LeakingObject());
18             try {
19                 Thread.sleep(100);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23         }
24     }
25 }
26 
27 class LeakingObject {
28     public byte[] largeArray;
29     private String data;
30 
31     public LeakingObject() {
32         largeArray = new byte[1024]; 
33         data = "This is LeakingObject";
34     }
35 }

MemLeakクラスのleakメソッドでは、LeakingObjectのインスタンスを無限ループ内で生成し、ArrayListクラスのインスタンスであるobjectsに追加しています。ArrayListはリストに含まれる要素への参照を保持するため、objectsに追加されたLeakingObjectクラスのインスタンスへの参照が残り続け、GCの回収対象となりません。その結果、ヒープメモリが徐々に消費され、最終的にはOutOfMemoryErrorが発生する可能性があります。

このメモリリークを解決するためには、次の手順で分析する必要があります。

1. リークしているクラスがLeakingObjectであることを特定
2. ArrayListクラスのインスタンスであるobjectsがLeakingObjectクラスのインスタンスへの参照を保持し続けていることを特定

「3.1. リーククラスの特定」、「3.2. オブジェクトの参照関係の追跡」で手順の詳細について説明します。

3.1. リーククラスの特定

Javaヒープでオブジェクトの数とメモリサイズが継続的に増加しているクラスを調べることで、リークしているクラスを特定できます。そのためには、時間をおいてクラスヒストグラムを複数回取得し、各クラスのオブジェクトの数とメモリサイズの増減を調べます。採取する間隔はアプリケーションによりますが、複数回のFullGCが発生する程度の時間を目途に調整してください。

以下のコマンドを実行することで、Javaヒープのクラスヒストグラムを出力できます。

jcmd <プロセスID> GC.class_histogram

以下にクラスヒストグラムの出力例を示します。各行にクラスごとのオブジェクトが表示され、"#instances"列にはオブジェクト数が、"#bytes"列にはオブジェクトが使用しているメモリサイズが表示されます。クラスヒストグラムはオブジェクトが使用しているメモリサイズの順で表示されます。

1回目に採取したヒストグラム

  num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:         13835        1168184  [B (java.base@21.0.5)
   2:         12655         303720  java.lang.String (java.base@21.0.5)
   3:          2309         280920  java.lang.Class (java.base@21.0.5)
   4:          2490         169784  [Ljava.lang.Object; (java.base@21.0.5)
~
  28:           335           8040  com.fujitsu.demo.LeakingObject
  29:           114           7296  java.util.concurrent.ConcurrentHashMap (java.base@21.0.5)
  30:           167           6680  java.lang.invoke.DirectMethodHandle (java.base@21.0.5)

2回目に採取したヒストグラム

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:         19656        7442056  [B (java.base@21.0.5)
   2:         12451         298824  java.lang.String (java.base@21.0.5)
   3:          2279         277360  java.lang.Class (java.base@21.0.5)
   4:          2362         202216  [Ljava.lang.Object; (java.base@21.0.5)
   5:          6377         153048  com.fujitsu.demo.LeakingObject
~

この例では、"byte"配列を示す"[B"と"com.fujitsu.demo.LeakingObject"のオブジェクト数とメモリサイズが増加していることがわかるため、リークしている可能性があります。次に、リークの原因となっている参照元を特定するために参照関係を調べます。

3.2. オブジェクトの参照関係の追跡

オブジェクトの参照を調べるために、実行中のアプリケーションのヒープダンプを採取します。

以下のコマンドを実行することで、ヒープダンプファイルを採取できます。

jcmd <プロセスID> GC.heap_dump <ファイル名>

採取したヒープダンプを解析するためのツールは複数ありますが、本稿では、JDK Mission Control(以降、JMC)を使います。[Eclipse Mission Control(「Eclipse Adoptium」のページ)]から、使用する環境に合わせたJMCをダウンロードして展開します。 本稿では、バージョン8.3.0を使用して説明します。

Windows環境で使用する場合は、以下の手順でJMCを起動します。

(手順1)JMCを起動するJavaを環境変数に設定

set PATH=<JDKのインストールディレクトリ>\bin;%PATH%

(手順2)jmc.exeを実行

<JMCのインストールディレクトリ>\JDK Mission Control\jmc.exe

JMCでヒープダンプを読み込むときは、JMCを起動後、「ファイル」メニューの「ファイルを開く」から解析したいファイルを指定してください。ヒープダンプファイルを開くと、「JOverflow」ウインドウが開かれます。

オブジェクトの参照関係の追跡は、次の「調査するオブジェクトを選別する」、「選択したオブジェクトの参照元を調べる」、「詳細な参照関係を調べる」の順で実施します。

調査するオブジェクトを選別する

図1 ヒープダンプに含まれるオブジェクト

左ウインドウの「All Objects」を選択することで、ヒープダンプに含まれるすべてのオブジェクトを表示できます。 参照関係を調べるオブジェクトの候補は、「3.1 リーククラスの特定」で特定したリーククラスのオブジェクトです。図1の例では、"byte\[]"のオブジェクトが25,103個残存し、Javaヒープの82%と大部分を占めていることがわかります。
また、"com.fujitsu.demo.LeakingObject"のオブジェクトが24,601個残存しています。クラスヒストグラムで継続的に増加していることを確認したオブジェクトが、ヒープダンプでJavaヒープの大部分を占めていることがわかりました。これらのオブジェクトがJavaヒープに多く残っている原因を特定するためにオブジェクトの参照元を調べます。

選択したオブジェクトの参照元を調べる

図2のように、表示されたオブジェクトから"byte\[]"を選択すると、右下のウインドウに選択したオブジェクトの参照元の一覧が表示されます。参照元の一覧には以下が表示されます。

  • オブジェクトへの参照を保持する変数
  • オブジェクトへの参照を要素に持つ配列およびコレクション[(注1)]

図2 ヒープダンプに含まれるbyte配列の参照元

図2の例では、"byte\[]"を選択した結果、"com.fujitsu.demo.LeakingObject.largeArray"から最も多くの24,601個の"byte\[]"のオブジェクトが参照されていることがわかります。

  • (注1)
    コレクションとは、複数のオブジェクトを格納、管理するためのフレームワークです。java.util.Collectionの実装クラスとして提供されます。java.util.ArrayListなどが該当します。

詳細な参照関係を調べる

次に、"byte\[]"のオブジェクトを最も多く参照していた"com.fujitsu.demo.LeakingObject.largeArray"の参照元を調べます。"largeArray"はさらに別の変数や配列、コレクションから連鎖的に参照されている可能性があるからです。 図2の右下のウインドウの一覧から"com.fujitsu.demo.LeakingObject.largeArray"を選択することで、右上のウインドウに参照の連鎖が表示されます。

図3 ヒープダンプに含まれるLeakingObjectの参照元

図3の例では、"com.fujitsu.demo.LeakingObject.largeArray"への参照として、"{ArrayList}"が表示されています。これは、"java.util.ArrayList"を意味する"{ArrayList}"の要素として、参照が保持されていることを示します。
"{ArrayList}"への参照として、"com.fujitsu.demo.MemLeak.objects"が表示されています。これは、MemLeakクラスに属するobjectsフィールドが参照を保持していることを示します。 "com.fujitsu.demo.MemLeak.objects"への参照として、"Java Local"が表示されています。"Java Local"はローカル変数が参照を保持していることを示しますが、どのローカル変数を示しているかはヒープダンプだけではわからないので、コードを確認します。「1. Javaのメモリリーク」のコードを確認すると、コードの10行目、MemLeakクラスのmainメソッドで作成されたローカル変数memLeakにMemLeakクラスのオブジェクトが格納されています。

以上から、"byte\[]"のオブジェクトを最も多く参照している"com.fujitsu.demo.LeakingObject.largeArray"の参照をたどると、MemLeakクラスのmainメソッドで作成されたローカル変数から参照されていることがわかりました。

クラスヒストグラムとヒープダンプを採取する時の注意

jcmdでクラスヒストグラムまたはヒープダンプを採取する時に、コマンドラインオプションに"-all"を指定しないでください。"-all"を指定すると、採取する前にGCが実行されなくなります。

4. おわりに

OpenJDKには、トラブルシューティングに役立つ便利なツールが豊富に用意されています。本稿では、Javaアプリケーションにおけるメモリリークに焦点を当て、OpenJDKに付属しているjcmdと外部ツールJMCを使って分析する手順を解説しました。本記事を安定稼働の参考にしてください。

次回は、JFRを使った分析方法を紹介します。

本コンテンツに関するお問い合わせ

Webでのお問い合わせ

入力フォーム 

当社はセキュリティ保護の観点からSSL技術を使用しております。

ページの先頭へ