連載
» 2007年12月27日 00時00分 UPDATE

現場から学ぶWebアプリ開発のトラブルハック(9):JavaのGC頻度に惑わされた年末年始の苦いメモリ (3/3)

[茂呂範,株式会社NTTデータ]
前のページへ 1|2|3       

【第5話】犯人はお前だ! Finalizer!!

 先ほどの4つのGCグラフを比較すると、明らかにFull GC(GCグラフ内の黒い縦線)の出方に違いがあることが分かった。

 「Finalizerの仕業か!」

 同僚の言葉に、私もうなずかざるを得なかった。

Finalizerとは何者か?

 さてここで、簡単にFinalizerについて説明しよう。Finalizerは、Javaオブジェクトのfinalizeメソッドを呼び出すスレッドだ。

 通常、ルートセットからたどれなくなったオブジェクトは、GCの対象となる。このとき、finailizeメソッドが定義されていなければ、そのままJavaヒープ上からは削除される。しかし、finalizeメソッドが定義されていると、一度finalize対象リストへと格納され、すぐにはJavaヒープ上からは削除されない。

 削除されるタイミングは、Finalizerスレッドによりfinalizeメソッドが呼び出された後の、GCのときだ。

finalizeメソッドがないときのJavaオブジェクト

 次の図8は、通常のJavaオブジェクト、すなわちfinalizeメソッドが定義されていないJavaオブジェクトの場合のライフサイクルを表す。

図8 通常のJavaオブジェクトのGCの様子 図8 通常のJavaオブジェクトのGCの様子
  1. オブジェクトがアプリケーションを実行するために利用されている間は、スレッドルートセット)から参照されるため、GC対象とはならない
  2. オブジェクトが不必要となったとき、すなわちスレッド(ルートセット)からの参照が外されるとき、そのオブジェクトはGC対象となる
  3. GCが実行されるときに、オブジェクトはJavaヒープ上から完全に削除される

 では、finalizeメソッドが定義されたオブジェクトはどうだろうか。

finalizeメソッドがあるときのJavaオブジェクト

 次の図9は、finalizeメソッドが定義されたJavaオブジェクトのライフサイクルを表す。

図9 finalizeメソッドが定義されたJavaオブジェクトのGCの様子 図9 finalizeメソッドが定義されたJavaオブジェクトのGCの様子
  1. オブジェクトがアプリケーションを実行するために利用されている間は、ルートセットから参照されるため、GC対象とはならない
  2. オブジェクトが不必要となったとき、すなわちルートセットからの参照が外されるとき、そのオブジェクトはGC対象となる
  3. GCが実行されるときに、オブジェクトはfinalize対象リストへと登録され、ルートセットから参照されることとなる
  4. Finalizerスレッドが、オブジェクトのfinalizeメソッドを実行し、再度ルートセットからの参照が外され、GC対象となる
  5. GCが実行されるとき、今度こそオブジェクトはJavaヒープ上から完全に削除される

 このように、finalizeメソッドが定義されたJavaオブジェクトがJavaヒープ上から削除されるためには、少なくとも2回以上のGCが必要だということが分かるだろう。

少なくとも2回以上のGCが必要なのに……

 先ほどの4つのGCグラフをもう一度見てほしい。メモリリークと思われていた、すなわちヒープ使用量が上昇していたGCグラフ(図2)は、2回以上Full GCが発生していない。

 また、ヒープ使用量が上昇していないGCグラフは、かなりの頻度でFull GCが発生していることが分かるだろう。

【第6話】GCのやっていることは全部お見通しだ!

 Finalizerが怪しいところまでは突き止めた。後は再現試験を実施するだけだ。再び同じように検証を行って、2度目のFull GCが発生したときに、ヒープサイズ大きく減少していればFinalizerの仕様が原因であるといえるだろう。

 早速、試験を行った。今回の試験は、Full GCの発生を促すために、最大ヒープサイズを非常に小さく設定して行った。

図10 GCグラフ:Finalizer再現試験 図10 GCグラフ:Finalizer再現試験

 1度目のFull GC(グラフ内では黒い縦線)では、オブジェクトはそれほど回収されておらず、2度目のFull GCで大きく回収されていることが分かる。そしてその幅が徐々に小さくなっていることが分かる。

 この再現試験の結果をもって、われわれの役目は終わった。メモリリークではない、Full GCの発生頻度を上げればよい、というところまで分かれば、後はプロジェクトメンバがどのような対策を実施するかを決定するだけだ。

 帰って雑煮でも食べることにしよう……

Full GC頻度の「少なさ」によるトリック

 今回は、Full GCの頻度が少なかったために発生したトラブルを紹介した。GCによるトラブルというと、Full GCの実行時間が長過ぎる、Full GCの頻度が多過ぎる、などが挙げられると思う。

 そのため、Full GCの「多さ」や「長さ」を気にすることはあっても、「少なさ」を気にすることは少ないのではないだろうか。しかし、今回の事例のように、Full GCの頻度が少な過ぎるために発生するトラブルもあることを知っていただければと思う。

【注意!】Finalizerが引き起こす3つのトラブル

 Finalizerに関する挙動は、Javaの中でも厄介な部類に入るだろう。今回は事例として紹介しなかったが、Finalizerに関しては、次のようなトラブルも発生する。簡単に紹介しよう。

【1】Finalizerメソッドの処理が重いことによるOutOfMemoryError

 Finalizerスレッドによる後処理のスピードがGCのスピードに追い付かず、finalize待ちオブジェクトがいつまでたっても解放されないために、メモリを圧迫するようになりOutOfMemoryErrorが発生する。

【2】Finalizerスレッド・デッドロックによるOutOfMemoryError

 Finalizerスレッドがデッドロックにより停止したため、finalize待ちオブジェクトが開放されず、メモリを圧迫するようになりOutOfMemoryErrorが発生する。

【3】ヒープサイズのフットプリントの上昇(厳密にはトラブルでない)

 finalizeの対象となるオブジェクトが多いと、それだけFull GC時に回収されるオブジェクトが少なくなる。これは、ヒープサイズのフットプリントを大きくし、プログラム動作により多くのメモリ量を必要とすることになる。

【最後に】「先入観」「思い込み」「事実と推測を混同」

 このように、Finalizerはメモリ回りのトラブルを引き起こしやすい、厄介な存在だ。だからといってFinalizerに頼るような設計を行うべきではない、というのは簡単だが、現実的には標準API内でも多用されている以上、Finalizerに頼らずにソフトウェアを構築することは難しい。

 やはり、Javaを利用している以上、GCの動作には常に注意を払っておき、それをもって、トラブルを未然に防ぐための予防としたい。

 ところでお気付きかもしれないが、本稿は、トラブル解析の際にちょっとだけ失敗した事例の紹介だ。どこで失敗したかはご想像にお任せする。

 キーワードは、「先入観」「思い込み」「事実と推測を混同」だ。これらのキーワードに引っかかったままトラブルハックを行うと、痛い目に遭うということを思い知らされる苦い思い出だが、良い事例だった。

プロフィール

茂呂 範(もろ すすむ)

株式会社NTTデータ 基盤システム事業本部所属。入社時よりOSSを用いたWebシステムの開発支援にかかわる。最近では、トラブルシューティングとその際のノウハウの収集・展開に日々従事している。



前のページへ 1|2|3       

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。