はじめに
初めてメモリリークに遭遇したため、対処方法を勉強してみました。
環境
- IntelliJ IDEA 2021.1.2 (Community Edition)
- Windows 10
- Java11
メモリリークとは
Javaプログラムの実行中にJVMのヒープ・メモリが不足することにより、
OutOfMemoryエラーが発生し、プログラムの実行ができなくなる事象です。
通常であればGC(ガベージコレクション)で不要なメモリの解放を行ってくれますが、
GCで削除できないリソースが存在すると、リソースを使い果たしてしまうということがあります。
GCは不要になったオブジェクトを削除してくれますが、逆に不要扱いにならないオブジェクトがプログラムの実行のたびに増えていくと、GCで捨てられるものがないために利用できるヒープ領域がどんどん減っていきます。不要扱いにならないオブジェクトを見つけ出すことがメモリリークの解消方法と言えそうです。
対応方法
今回はIntelliJ公式のチュートリアルに沿って実践してみます。
https://pleiades.io/help/idea/tutorial-find-a-memory-leak.html
事前準備と実行確認
-
チュートリアル用のソースコードをGitでクローンします。
https://github.com/flounder4130/party-parrot
クローンが完了したら、IntelliJでソースコードを開きます。
初回はビルドに1~2分かかると思います。 -
「Alt+Shift+F10」(「構成を選択して実行」のショートカット)を押下し「parrot」を選択します。
上手くいかない場合は、「src/main/java/Parrot.java」を右クリック⇒実行をしてください。
動くオウムのイラストが表示されます。
-
スクロールバーやRainbow Modeでオウムの色や動きを変えてみましょう。
メモリ使用量の分析
チュートリアルに記載のプロファイラー機能ですが、無料版のCommunityでは利用ができません!
筆者はCommunity版のため、チュートリアルの手順が実施できませんでした。
そのため、ここからは別の手順で確認を進めてみます。
(Ultimate版の人は引き続き、チュートリアルの手順通り進めてみてください)
-
VisualVMをダウンロードします。
https://visualvm.github.io/
今回はスタンドアローンでダウンロードをしてみます。
ダウンロードが完了したら任意のディレクトリに展開しておきます。 -
ファイル⇒設定⇒プラグインで「VisualVM Launchar」を検索し、インストールをします。
VisualVMはメモリやCPUの使用率を確認することができる機能です。
VisualVM Launcharを入れるとIntelliJのツールバーからボタン1つで起動確認ができます。 -
設定画面が表示されるため、VisualVM Executableを入力しておきます。
「先ほどVisualVMを展開したディレクトリパス/bin/visualvm.exe」を選択してください。
それ以外はデフォルトで実行します。
※設定を変更したい場合は、「ファイル」⇒「設定」⇒「VisualVM Launchar」から変更が可能です。
規約の画面が出てくるのでacceptをすると、VisualVMが起動します。
Local java applications caonnot be detected. が表示された場合
C:/Users/[your username]/AppDate/Local/Temp/hsperfdata_[your username]
を削除して、IntelliJから再度VisualVMを起動してみてください。
※AppDateは隠しファイルのため、エクスプローラーから「表示」⇒「隠しファイル」にチェックをつける必要があります。
- グラフの見方を確認します。
まずは、左側のメニューから「Parrot」を選択して、右側の「Monitor」タブを開いてみます。
以下はアニメーションが停止するまで実行した状態のグラフです。
右上のグラフを見ると、Used Heap(青色部分)が右肩上がりになっていることがわかります。
最終的にはUsed heapがヒープサイズのMaxまで到達しており、このタイミングでJava側のイベントログを確認するとOutofMemoryが発生したことが読み取れます。
山になっている部分について、上がっている時はオブジェクトが生成されているタイミングです。一方で下がっている時はGCが実行されているタイミングとなります。
以下のグラフだとオブジェクトの生成タイミングが多いため山が頻繁にできており、GCが実行されているものの追い付いていないことがわかります。
- それでは実際にメモリに影響を与えているクラスの特定をしていきます。
「Parrot」を再度実行をしたら、すぐにMonitorの右上にある「Heap Dump」を押下します。
[heapdump]というタブを開き、Heap Dumpのメニューから「Objects」を選択してみましょう。
各クラスのメモリ使用量を確認することができます。
実行直後とOutofMemoryが発生したタイミングの状態を比較することで、
メモリを多く使用しているクラスを特定することが可能です。
OutofMemory状態のダンプファイルを確認すると、int[]が悪さをしているようです。
「+」を押していくと参照しているクラスを確認することができます。
「BufferdImage」というクラスが出てきました。
Parrot.javaを確認すると、「BufferdImage」を確かに利用していることがわかります。
BufferdImageを利用している部分を確認していくと、以下の記載が怪しそうです。
というのも、メモリリークはstatic変数やHashMapを利用している箇所でよく発生するようです。
今回は以下の記事に記載の通り、HashMapにequals()とhashCode()が適切にオーバーライドされていないことが原因と言えそうです。
https://ja.getdocs.org/java-memory-leaks
※勉強不足のため、このあたりはまた別の記事で扱おうと思います。
-
今回はStateクラスにequals()とhashcode()を正しく実装してみます。
equalsと打つと候補が表示されます。
候補を選択するとメソッドの生成に関する設定画面が表示されるため、全ての画面についてデフォルトで「次へ」を押します。
-
メソッドが実装できたらもう一度実行をしてみます。
メモリが一定ラインから上がらなくなり、OutofMemoryが発生しなくなりました!
さいごに
IntelliJのCommunity版(無料版)でも分析ができるのはありがたい発見でした。
メモリリークが発生した際の対処方法を理解するのももちろんですが、メモリリークを行さないような実装も今後学ぶ必要がありそうです。