先々週にHotSpot VMでのメモリー管理について解説しました。ここでキーとなるのは世代別GCです。

HotSpot VMで世代別GCが採用される以前は,Old領域のGCで使用されるMark & Sweep GCだけでした。世代別GCが導入されたことにより,GCのパフォーマンスは劇的に向上したのです。

しかし,GCの進化はここで終わってしまったのではありません。Java SE 6(開発コード名Mustang)にいたるまで,様々な改良が加えられてきました。

今週はそれらの新しいGCの手法について解説していきます。その前に,まずは基本となるMark & Sweep GCを説明しましょう。

Mark & Sweep GC

Mark & Sweep GC
図1 Mark & Sweep GCの動作

Mark & Sweep GCは二つのフェーズでGCを行います。

はじめのフェーズで,使用しているインスタンスに印をつけます(Mark,図1a)。Markにはルートインスタンスから参照をたどるという方法が使用されます。

次のフェーズでは,使用していないインスタンスを廃棄し(図1b),ヒープが虫食いにならないようにインスタンスを配置し直します(Sweep,図1c)。

これらのフェーズを遂行している間はすべてのスレッドは停止しています。そして,GCが終了したら,アプリケーション・スレッドが再び動作を開始します。

完全にアプリケーション・スレッドが停止してしまうため,アプリケーションも停止してしまいます。HotSpot VM以前のJava VMではこの停止期間が長く,Javaが遅いという風説の一因になっていました。

世代別GCの採用により,状況は一変します。ただ,はじめの印象というのは強く,いまだにJavaは遅いと思っている人が多くいるのは残念なことです。

Young領域に対するパラレルGC

シリアルGCとパラレルGC
図2 シリアルGCとパラレルGC

パラレルGCというのは,GCを複数のスレッドを使用してパラレルに行うものです(図2)。複数のスレッドを使用することで,GCの時間を短くし,スループットを向上することができます。

パラレルGCはシングルCPUでは効果がなく,マルチCPUで威力を発揮します。最近のCPUのトレンドはマルチコア/メニーコアなので,パラレルGCが有効な場面も増えています。

JavaでのパラレルCGの採用はJ2SE 1.4.1からです。

J2SE 1.4.1でYoung領域のコピーGCがパラレル化されました。コピーGCはもともと処理時間が短いのですが,パラレルGCを使用することにより,GCの処理時間はさらに短くなっています。

Mostly Concurrent Mark & Sweep GC

Mostly Concurrent Mark & Sweep GC
図3 Mostly Concurrent Mark & Sweep GC

Mostly Concurrent Mark & Sweep GCもJ2SE 1.4.1で採用されたGCです。

Mark & Sweep GCは前述したようにアプリケーション・スレッドをすべて停止して行います。これに対し,Mostly Concurrent Mark & Sweep GCはGCのほとんどの過程をアプリケーション・スレッドと並列に行います。

ほぼ並列に行うことができるので,Mostly Concurrentというわけです。

図3にMostly Concurrent Mark & Sweep GCの動作を示します。

並列化できないのはMarkフェーズの一部の処理で,図3ではinitial markとremarkの部分です。

前述したようにMarkには,ルートインスタンスから参照をたどっていきます。Markを並列で行う場合,Markした時からSweepを行うまでの間に生成されたインスタンスがMark漏れしてしまいます。

このようなインスタンスを検出するために,アプリケーション・スレッドを停止して漏れを防いでいます。

しかし,この処理はMarkやSweepに比較すると,短時間で終わらせることができます。

Mostly Concurrent Mark & Sweep GCを使用することで,スループットとレスポンスの両方を向上することが可能です。

Old領域に対するパラレルGC

前述したパラレルGCはYoung領域に対するGCでした。

Java SE 5.0 update 6からはOld領域に対してもパラレルGCが行われるようになりました。

J2SE 1.4.2まで,updateリリースはバグフィックスのみが行われてきました。Java SE 5からはこの方針が変更され,updateリリースでバグフィックスとパフォーマンス向上の両方が行われるようになっています。

この変更はマイナーリリースが行われなくなったということが理由にあると筆者は予想しています。

Old領域に対するパラレルGCはSweepフェーズが対象です。並列してアプリケーション・スレッドが動作しているので,GCの複数のスレッドのバランスが重要になります。

エスケープ解析

GCではないのですが,メモリー管理に関連する技術としてエスケープ解析を最後にご紹介しましょう。

皆さんがご存じのように,Javaではインスタンスはすべてヒープにアロケーションされます。それでは,プリミティブ型のローカル変数はどこに保持されるのでしょう。

答えは,スレッドごとに用意されているスタックに保持されます。スタックにはGCがなく,管理も楽に行うことができます。

C++も同様にスタックを使用しており,スタックにインスタンスをアロケーションすることも可能です。

JavaでもC++のようにインスタンスをスタックにアロケーションできればと思いますよね。なんと,それがJava SE 6から可能になります。

ところが,Javaではスタックにアロケーションするかヒープにアロケーションするかをアプリケーション側で選択できません。すべて,Java VMが判断してアロケーションする場所を決定します。

このときに使用される技術がエスケープ解析です。

判断の基準になるのはインスタンスがメソッドの中だけで使用されるかどうかです。例えば,次の例を見てみましょう。

  public String concatenate(Sting text1, String text2) {
    StringBuilder builder = new StringBuilder();
  
    builder.append(text1);
    builder.append(text2);
    String resultText = builder.toString();
 
    return resultText;
  }

このメソッドでインスタンスは四つあります。引数のtext1とtext2はメソッドの外側から,このメソッドに入ってきます。またresultTextは戻り値としてメソッドの外側に出て行ってしまいます。

残ったbuilderだけがメソッドの中だけで使用されるインスタンスです。したがって,builderだけはスタックにアロケーションされる候補になります。

インスタンスがメソッドの中にとどまっているかどうか,逆にいえばメソッドから逃げ出さないか(エスケープしないか)が,スタックにアロケーションするかどうかの判別に使用されます。エスケープするか,しないかということでエスケープ解析と呼ばれます。

上記のconcatenateメソッドでのbuilderはスタックにアロケーションされる最も典型的なものです。他にはオートボクシングの過程で一時的に使われるインスタンスなどがあります。

エスケープ解析は解析のためのオーバヘッドが少なからずあります。このため,デフォルトではエスケープ解析はオフになっています。

今月のまとめ

今月はJavaのメモリー管理,特にGCについて解説を行ってきました。HotSpot VMで使用されているGCだけをご紹介しましたが,まだ世の中には様々なGCのアルゴリズムがあります。例えば,組み込み向けリアルタイムJava VMであるMackinacでは,HotSpot VMとは異なるGCが使われています。

Javaの特徴であるGCは,アプリケーション作者をメモリー管理から解放してくれました。その一方で,GCはその動作ゆえ,常に悪者扱いされてきました。

しかし,GCの動作原理を理解して,GCとうまくつきあうことができれば,これほど強力な味方はいません。

また,パラレルGCなどGCの進化も常に続けられています。もう,GCを悪者扱いするのは時代遅れです。

Javaだからメモリーは全く気にしないのではなく,その特性を理解して,GCとうまくつきあうことがプログラマには求められているのです。

著者紹介 櫻庭祐一

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