人によっては非常にありきたりのことが多いと思いますが、実際問題よくぶち当たることが多いというのと、Javaのチューニングというと、GCポリシー変更などのインフラ寄りのチューニング方法が多いので、アプリで実現できるTIPS的なものを中心に記載していきます(思い出したものから。。)。
パフォーマンス問題ケース
文字列連結の乱用
- 問題事象:OutOfMemoryError、速度悪化
- 原因:String文字列の大量生成
- 解決方法:最新のJDKでコンパイル、可変長文字列編集クラス(StringBuffer、StringBuilder)を使用など
詳細
具体的には以下のような記述です。よく見かけますね。
String 連結文字列 = "";
連結文字列 = 連結文字列 + 追加文字列1;
連結文字列 = 連結文字列 + 追加文字列2;
連結文字列 = 連結文字列 + 追加文字列3;
連結文字列 = 連結文字列 + 追加文字列4;
System.out.print(連結文字列);
(追加文字列1~4はString型の変数です。そもそも、+=を使えよというのもあると思いますが、結構この書き方をする人が多いんですよね。。。)
2、3個の結合であればよいのですが、5,6個の結合でも実行回数等によっては、問題が出る可能性があります。
上記の場合、Stringクラスのインスタンスが連結のたびに生成されるため、小さいJavaオブジェクトが大量に生成されることに伴い、GC発生回数に影響します。(GC発生回数増大⇒パフォーマンス悪化)
また、ループ文で大量生成された場合は、インスタンス数がJava HeapのNative領域を食いきってしまうということもあります。(これが発生するかは、JVMの実装依存)
対処方法は以下となります。
String 連結文字列 = "";
StringBuilder sb = new StringBuilder();
sb.append(連結文字列)
.append(連結文字列1)
.append(連結文字列2)
.append(連結文字列3)
.append(連結文字列4);
System.out.print(sb.toString());
(StringBuilderの生成コストは増えるものの、String生成コストの低下のほうが一般には効果が大きい)
-
最近のJavaはコンパイル時の最適化で上記のようなコードに最適化してくれることが多いようです。今後はこの辺のチューニングは不要となるかもしれません(まだ書き方などによっては最適化されないこともあるようですが、、、)
-
実装箇所をスレッドセーフとする必要がある場合は、StringBuilderの代わりにStringBufferを使用します。(スレッドセーフに対応している分、StringBufferのほうが少し重い)
-
結合回数があらかじめ一定以下でわかっている場合はStringBuilderの初期値を設定することで性能が上がることもあります
重いAPIの乱用(主にDate型)
- 問題事象:速度悪化
- 原因:Dateの大量生成
- 解決方法:実行回数を減らす
詳細
業務アプリケーションでよく使う型として、文字列の次は数値か日付だと思いますが、日付のほうで問題がよく発生します。というのも、Javaの場合DateをNewする時に稼動システムへのアクセスがあるため、生成コストが重いためです。
しかし、日付系はユーティリティクラスで扱うことが多く、かつユーティリティ内で日付生成(new Date)をおこなうことが多いため、項目数が多い業務アプリケーションで日付項目を多数扱うと、自動的に何度もnew Dateしてしまい、パフォーマンスが悪くなることがあります。
個人的な経験としては、たとえば以下のようなケースがあります。
- オンラインアプリケーションで総項目数が数千項目の内、日付項目が数百レベルであり、この日付項目の初期化でパフォーマンスが数秒程度悪い
- バッチアプリケーションで取り扱いデータ件数が数百万件あり、各データ毎に業務日付を更新するときに、基準日としてのシステム日付生成(Date 生成)でパフォーマンスが数時間レベルで悪い
この場合の解決方法としては、たとえば以下のような方法があります
- 日付ユーティリティクラスのメソッドにシステム日付としてDateを引数追加し、Dateの生成をユーティリティ内部ではなく外部で実施するようにする。これにより、同じシステム日付でよい場合はDateの生成回数を一回に抑える
- 引数追加が難しいが、実際にはユーティリティのインプット/アウトプットのバリエーションに偏りが多い場合、ユーティリティクラスにキャッシュ機構をもたせ、ユーティリティへの引数が同じ場合はキャッシュから情報をアウトプットすることで、New Dateを抑制(システム日付を元に、商品コードなどを元に3パターンの日付が生成される、などのケースを想定)
- そもそも、日付ユーティリティ関数を都度使う必要が無い場合は、それをやめる、、、(複数項目に同じ日付を設定すればよいのに、毎回ユーティリティで生成しているケース)
外部通信の速度低下(主にホストやDBなどの外部システム)
- 問題事象:外部通信先の資源不足、速度悪化
- 原因:外部通信先に不適切に?リクエストが飛んでいる
- 解決方法:キャッシュにより実行回数を減らす
詳細
ホスト(レガシーシステム)からユーザ情報や契約情報を取得する場合、都度取得をおこなうとホスト側がたれられない場合があります。この場合、ログイン時などに一回だけ取得をおこないメモリなどに情報を保持しておきます。普通のシステムではやっていることだとは思いますが、たまに特定処理だけ抜けていたり、新たに通信が発生した場合に、そういったことの考慮が抜けていると問題になることがあります。。。
同様にDB通信についても、マスタデータなどのログイン中は不変ととらえられるものについて、キャッシュ化することがあります。キャッシュ化にあたっては、単純にセッションデータへ格納しユーザ毎に持つ場合と、シングルトンクラスなどに保持し、全ユーザで共通に持つ場合の2パターンがあります。
これらは、メモリ使用量の増加というトレードオフがあるため、キャッシュ量への考慮は別途必要です。
また、昔はDBサーバネックになることが多かったのですが、最近はDBサーバの性能もどんどんよくなっているほか、JPAなどキャッシュ機構をあらかじめ持っている場合も多いとは思います。
ファイル操作など低レベルIO負荷が大きい
- 問題事象:巨大なファイルなどを扱う場合に、非常に遅くなる、OutOfMemoryErrorが発生
- 原因:使用するAPIが不適切、扱うデータ量が大きく、JVMのオーバヘッドが致命的になっている
- 解決方法:使用するAPIを最適なものにする、OS処理にゆだねる
詳細
主にJavaバッチなどで、データ量が数百Mや数G以上のファイルなどを扱う場合にパフォーマンス問題や、そもそもOutOfMemoryErrorで処理できないなどのケースがあります。
対処方法としては、以下のようなケースが考えられます。
- そもそもLeadLineなどで1行ずつ律儀に処理している場合は、ストリームクラス(nioクラスなども含む)などでの(バッファ単位での)一括処理を考えます。こちらの場合、バッファサイズのチューニングは必要な可能性はありますが、これで解決できるのであれば問題なしです(元のコードが問題ともいう)
- 上記では十分な速度が出ない場合、Java内からOSコマンドを実行することで劇的に解決できることがあります。たとえば、Aファイルの後ろにBファイルを単純にファイル連結(コンカチ)したい場合であれば、以下のようなコードになります(catコマンドを使用)。
Runtime runtime = Runtime.getRuntime();
runtime.exec("cat Aファイル Bファイル > 連結ファイル");
(コンカチのほかにはファイル比較でdiffコマンドを使用、文字列置換でawkを使用などもありえます)
この方法で問題なのはOSへの依存性を持つということで、ポータビリティへの制約がでてきます。最初から全箇所で適用とするのではなく、最低限の箇所への適用としたいです。(テスト環境はWindows、本番はUNIXなどの場合は、該当実装箇所は環境別にコマンドを返られるようにする必要がある、など)
- そもそも該当処理はJavaバッチではなくシェルバッチ化する、、、
不必要な同期処理
- 問題事象:Javaの同期処理により、複数ユーザアクセス時のパフォーマンス悪化
- 原因:不適切な範囲での同期処理が実装されている
- 解決方法:適切な範囲に同期処理を狭める
詳細
ユーティリティクラス等でシングルトンなどを扱うために同期処理(synchronizeブロック)を保持したメソッドがあり、これを単純コピーして別メソッドを作ったときに、同期処理が不要なメソッドであるにもかかわらず、synchronizeブロックが記述されている。ないしは安全にしすぎて、synchronizeブロックの範囲が広すぎて、ロック発生が非常に発生しやすくなっている、などのケースがあります。。。
対応は単純に、以下となります。
- 不要なsynchronizeブロックを削除
- 適切に広いsynchronizeブロックを適切な範囲に狭める
ですが、synchronizeブロックを狭めすぎると、今度はスレッドセーフ問題になってしまうため、対応は非常に慎重におこなう必要はあります。 問題が発生していなければ、不要な記述があっても対応しないというのも手ではあります。。
処理の非同期化
- 問題事象:非常に処理が重いが、小手先では対応できない。ただし、同期処理要件はない。
- 解決方法:処理タイミングをずらす
詳細
ほぼアーキテクチャレベルでの変更になりますので、ここまで踏み込むことは避けたいですが、実際にはパフォーマンステスト結果を受けて実施することもあると思います。
- 処理を前にずらす(マスタデータのキャッシュタイミングをJVM起動タイミングとする、中間DBをバッチであらかじめ生成しておく)
- 処理を後ろにずらす(メッセージングシステムなどを使用、別スレッド化などにより非同期処理化をおこなう、オンライン処理をあきらめ一部処理をバッチ化する)
処理の多重化
- 問題事象:非常に処理が重いが、処理の並列化が可能。また、サーバ資源的には余裕がある
- 解決方法:処理を多重化する
詳細
バッチを想定していますが、オンラインでも非同期化と組み合わせて実施される場合も一部あると思います。
多重化想定はあるアプリケーションについては、最適な多重度を探るだけとなりますが、多重度想定が無いアプリケーションに対して対応する場合は、こちらもアーキテクチャレベルでの変更となるため、大改造となります。。。
パフォーマンス問題箇所の特定
ログでの情報収集
アプリそのものや、WEBサーバのアクセスログ、外部通信先などのログなども含めて、どの処理が重いのかを確認します。
基本的にはログ確認が最優先となると思います。(本番ではログを出せないなどで、ここが一番難しかったりしますが、、)
GCログh
これもログですが、Javaの場合はGCでのパフォーマンス劣化はもっともよくあるケースですので、特出ししています。JVMにもよると思いますが、一般的にはGC頻度やその種類(Full GCの有無)、GC時間などが確認ポイントになるかと思います。
OSレベルでの情報収集
各OS用のパフォーマンスモニターが用意されていると思いますので、CPUやメモリ、OSアクセスなどを確認します。Javaアプリの場合、基本的にはOSのメモリネックは無いはずなので、CPUネックかどうかの特定が主になります。CPUが100%だとしても、メモリ不足に伴うGC多発に伴うCPU使用量高騰だったりもしますので、これだけで何かがわかるわけではないのですが、、、
JVMレベルでの情報収集
スレッドダンプを取得できる場合は、パフォーマンス問題状態でのスレッドダンプを取得することで、Java内部の状態を確認することができます。文字列系の問題であれば、Heap内のオブジェクト数の大多数がString文字列で占められている、同期処理系の問題であれば、スレッド情報にsynchronize句のあるメソッドが多数占められている、などがわかります。
ほかにも、JVMによってはJavaのオブジェクト情報の詳細情報を取得できる場合などもあります。