皆さんは,ご自分で作成されたアプリケーションでどのくらいの頻度でガーベジ・コレクション(GC)が発生しているか認識されていますか。まずは,このGCの発生頻度から調べてみましょう。

GCの発生頻度を調べるにはjavaの起動オプションに-verboseを使用します。-verboseだけだとクラスローディングやネイティブライブラリの使用に関する情報も表示されてしまうので,GCだけに特化したいときには-verbose:gcとします。

先週も使用した,JDKのサンプルのJava2Demoでやってみましょう。

> java -verbose:gc -jar Java2Demo.jar
[GC 512K->216K(1984K), 0.0089257 secs]
[GC 726K->486K(1984K), 0.0281309 secs]
[GC 997K->635K(1984K), 0.0097482 secs]
[GC 1147K->912K(1984K), 0.0034273 secs]
[GC 1376K->1115K(1984K), 0.0021296 secs]
[GC 1627K->1332K(1984K), 0.0022816 secs]
[GC 1800K->1485K(2112K), 0.0008311 secs]
[Full GC 1485K->1332K(2112K), 0.1032614 secs]
[GC 1844K->1544K(2800K), 0.0013253 secs]
[GC 2054K->1607K(2800K), 0.0009476 secs]
[GC 2119K->1676K(2800K), 0.0020701 secs]
[GC 2187K->1760K(2800K), 0.0013180 secs]
[GC 2272K->1881K(2800K), 0.0060524 secs]
    ... 以下,延々と続く ...

たぶん,予想していたよりも多数回のGCが発生していたのではないでしょうか。

GCというと,アプリケーションの動作が一瞬止まってしまう印象が強いようですが,Java2Demoはほとんど止まることなく実行できるはずです。Java2Demoはあくまでもサンプルであり,それほど大きいアプリケーションではありません。

それでも,これだけGCが発生しているのですから,表示が一瞬止まることも予想されます。

上記の-verbose:gcによる表示では,GCもしくはFull GCと表記された後に,二つの数字が矢印で示されています。これはGC前のヒープ使用量とGC後のヒープ使用量を示しています。カッコの中の数字はヒープの空き容量を示しています。最後の数字がGCに要した時間を表しています。

このGCに要した時間をよくご覧ください。

単にGCと表記されているものと,Full GCと表記されているものでは,GCに要した時間に大きく隔たりがあることがわかります。

単にGCと記述されているものは数ミリ秒から数十ミリ秒,Full GCでは数百ミリ秒とオーダーが全く違います。

つまり,単にGCと記述されているGCでの処理の停止時間は全く問題にならないことになります。逆にいうと,頻繁にFull GCが発生するようなアプリケーションは何らかのメモリー・チューニングを施さないと,アプリケーションが頻繁に停止することにつながってしまいます。

実をいうと,この時間の違いにこそ,大きな秘密が隠されているのです。

GCとだけ記述されているものはScavenge GCと呼ばれ,Full GCと表記されているものはそのままFull GCと呼ばれます。そして,この二つのGCは,アルゴリズムが全く異なる異種のGCなのです。

この違いを理解するには,まずHotSpot VMでのヒープの管理について知っておく必要があります。

HotSpotでのヒープ管理

ヒープの構成
図1 ヒープの構成
インスタンスのアロケーション
図2 インスタンスのアロケーション
1回目のGC
図3 1回目のGC
2回目のGC
図4 2回目のGC
3回目のGC
図5 3回目のGC

HotSpot VMではインスタンスの寿命を元にヒープの管理を行います。インスタンスの寿命というのは,インスタンスがGCから生き残った回数を指しています。

つまり,寿命が長いインスタンスというのは,インスタンスが存在している間に何度もGCが発生していることを意味しています。

HotSpot VMは寿命の短いインスタンスと長いインスタンスを分離して管理しています。寿命の短いインスタンスをYoung領域に配置し,長いインスタンスをOld領域に配置します(Young領域はNew領域と記述される場合もあります)。

Young領域とOld領域は,それぞれ異なるGC手法を用いて管理します。このような管理を総称して世代別GCと呼びます。

HotSpot VMでは,さらにYoung領域をEdenと二つのSurvivorという三つの領域に分割して管理します。Old領域はTenured領域と呼ばれます。

これを示したのが図1です。

すべてのインスタンスはまずEden領域にアロケートされます(図2)。Eden領域がいっぱいになると,1回目のScavenge GCが行われます。

そうです。Scavenge GCと呼んでいたのは,Young領域に対するGCなのです。

Scavenge GCでは,Eden領域の中で使用されていないインスタンスは消滅させ,生き残っているインスタンスを片方のSurvivor領域に移動させます(図3)。

これで,またインスタンスをEden領域にアロケーションできるようになります。

Eden領域が再びいっぱいになると,Eden領域にある生き残ったインスタンスは使用されていないSurvivor領域にコピーされます。そして,現在使われているSurvivor領域で生き残っているインスタンスがあれば,これももう一方のSurvivor領域にコピーされます(図4)。

この結果,今まで使われていたSurvivor領域は空になり,使われていなかったほうにすべてのインスタンスが移動します。

Survivorにあるインスタンスのうち,寿命のしきい値を超えたインスタンスはTenured領域に移動します(図5)。

このしきい値はYoung領域の使用状況によって変化します。インスタンスの生成量が多い場合,Young領域の使用量が大きくなるため,しきい値は低下し,寿命がより若くてもTenured領域に移動します。

このScavenge GCはオブジェクトを移動させるだけなのでCopy GCとも呼ばれ,高速にGCを行うことができます。

Tenured領域の残りがある一定値に達したら,ヒープ全体を対象にしたGCが行われます。このGCは基本的にはMark & Sweepです。

-verbose:gcの表記では,単にGCと表記されたものがCopy GC,Full GCと表記されたものがMark & Sweep GCになります。

このように二つのGCでは,アルゴリズムが全く異なり,その処理速度も全く違います。

この違いを認識しておかないと,ヒープのチューニングを行うこともできません。

来週は,Eden領域やSurvivor領域,Tenured領域がどのように使用されているかを調べてみることにしましょう。

著者紹介 櫻庭祐一

横河電機の研究部門に勤務。同氏のJavaプログラマ向け情報ページ「Java in the Box」はあまりに有名