徹底解剖「G1GC - narihiro.infog1gc...

49

Transcript of 徹底解剖「G1GC - narihiro.infog1gc...

Page 1: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc
Page 2: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

徹底解剖「G1GC」実装編(執筆中)

中村成洋著

2011-12-25 版 www.narihiro.info 発行

Page 3: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

i

スポンサーのみなさま

募集中

詳細は http://www.narihiro.info/g1gc-impl-book/を参照ください。

Page 4: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

ii

はじめに

注意事項

G1GCのアルゴリズムの一部は米国で特許が取得されています*1。G1GCを実装して公開する場

合は注意してください。

対象読者

OpenJDK 7のメモリ管理周りの実装に興味がある方、『徹底解剖「G1GC」アルゴリズム編』[2]

をすでに読んだ方を対象にしています。

図中の矢印

本書内の図にはさまざまな矢印が出てきます。本書で使う主要な矢印を図 .1にまとめました。

図 1 矢印のパターン

矢印 (a)は参照関係を表します。ルート*2からオブジェクトへの参照などに使われます。

矢印 (b)は代入操作、移動操作に表します。変数への代入・オブジェクトコピー・移動などに使わ

れます。

矢印 (c)は時間の経過を表します。

*1 米国特許番号は 7340494*2 ルート:オブジェクトのポインタをたどる際の「起点」となる部分。

Page 5: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

iii

目次

スポンサーのみなさま i

はじめに ii

第 1章 準備 1

1.1 HotspotVMとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

1.2 OpenJDKとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

1.3 ソースコード入手 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2

1.4 ソース構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

1.5 G1GCのアルゴリズム . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

第 2章 アロケータ 5

2.1 アロケーションの流れ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

2.2 VMヒープの予約 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

2.3 VMヒープの確保 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2.4 Windowsでのメモリ領域の予約、確保 . . . . . . . . . . . . . . . . . . . . . . . . 10

2.5 Linuxでのメモリ領域の予約、確保 . . . . . . . . . . . . . . . . . . . . . . . . . . 11

2.6 VMヒープのアラインメント実現方法 . . . . . . . . . . . . . . . . . . . . . . . . . 12

2.7 オブジェクトのアロケーション . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2.8 大型オブジェクトのアロケーション . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2.9 TLAB(Thread Local Allocation Buffer) . . . . . . . . . . . . . . . . . . . . . 16

第 3章 ヒープ構造 18

3.1 VMヒープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

3.2 CHeapObjクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

3.3 CollectedHeapクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

3.4 G1GCヒープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

3.5 HeapRegionSeqクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

3.6 HeapRegionクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

3.7 パーマネント領域 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

第 4章 オブジェクト構造 25

4.1 oopDescクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

Page 6: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

目次

4.2 klassOopDescクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

4.3 Klassクラス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

4.4 クラスの関係 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

4.5 oopDescクラスに仮想関数を定義してはいけない . . . . . . . . . . . . . . . . . . . 28

第 5章 正確な GCへの道(HotspotVM編) 30

5.1 プリミティブ型と参照型の変数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

5.2 HotspotVMの構造 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

5.3 HotspotVMの実行フロー . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

5.4 参照マップ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

5.5 抽象的インタプリタ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

5.6 参照マップの作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

5.7 条件分岐時の参照マップ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

5.8 メソッド呼び出し時の参照マップ . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

5.9 ハンドルエリアとハンドルマーク . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

付録 A 参考文献 42

iv

Page 7: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

1

第 1章

準備

HotspotVMとは何かという点から話を進めていきましょう。

1.1 HotspotVMとは

HotspotVM はサン・マイクロシステムズ社(現:オラクル)主導で開発されているもっともポ

ピュラーな JavaVMです。

HotspotVMの特徴は「プログラムの実行頻度の高い部分のみ機械語にコンパイルする」という点

です。これにはプログラムの実行時間を多く費やす部分(実行頻度の高い部分)を最適化し、プロ

グラム全体の実行時間を短くしようという狙いがあります。また、機械語へのコンパイルをある程

度の範囲に絞るため、コンパイル時間が短くなるという効果もあります。「実行頻度の高い部分」を

Hotspotと呼びます。この点が HotspotVMの名前の由来となっています。

HotspotVM のもう 1 つの特徴は複数の GC アルゴリズムが実装されているという点でしょう。

通常 GCアルゴリズムは、レスポンス性能とスループット性能のどちらかを優先してチューンアッ

プします。一般的にレスポンス性能を優先した GCアルゴリズムはスループット性能が低下します。

逆にスループット性能を優先した GCアルゴリズムはレスポンス性能が低下します。このようなジ

レンマがあるため、現在のところ完璧な GCは存在しません。HotspotVMはこのジレンマに対す

る回答として「複数の GCアルゴリズムを実装する」という手法をとりました。プログラマはアプ

リケーションの特性に合わせて HotspotVMの GCアルゴリズムを選択することができます。つま

り、レスポンス性能を求めるアプリケーションの場合は、それに適した GCアルゴリズムを「プロ

グラマ」が選択できるということです。プログラマによる GCアルゴリズムの選択は優れた手法だ

と言えるでしょう。

1.2 OpenJDKとは

Java の開発用プルグラミングツール群のことをまとめて「Java SE Development Kit(JDK)」

と呼びます。

JDKには HotspotVMの他に、Javaのソースコードを Javaバイトコードにコンパイルする Java

コンパイラ、Javaソースコードからドキュメントを生成するツール等が同梱されています。

Page 8: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 1章 準備 1.3. ソースコード入手

2006年 11月、当時のサンは JDKのソースコードを GPLv2*1の下で無償公開することを発表し

ました。このオープンソース版の JDKを「OpenJDK」と呼びます。

OpenJDKの最新バージョンは「OpenJDK7」と呼ばれています。一方、オラクルが正式に提供

する JDKの最新バージョンは「JDK7」と呼ばれています。OpenJDK7と JDK7は名前は異なる

ものですが、両者のコードはほぼ同じです。ただし、JDKにはライセンス的にクローズドなコード

が一部あるため、OpenJDKではそれをオープンソースとして書き直しているようです。

1.3 ソースコード入手

OpenJDKの公式サイトは次の URLです。

http://openjdk.java.net/

図 1.1 OpenJDK公式サイト

現在の最新のリリース版は OpenJDK7です。開発マイルストーンは次の URLで現在でも見るこ

とができます。

http://openjdk.java.net/projects/jdk7/milestones/

本章では OpenJDK7の最新バージョンである「jdk7-b147」を解説対象とします。

では、ソースコードをダウンロードしましょう。

OpenJDK7の最新開発バージョンのソースコードは次の URLからダウンロードすることができ

ます。

*1 GPLv2(GNU General Public License):コピーレフトのソフトウェアライセンスの第 2版

2

Page 9: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 1章 準備 1.4. ソース構成

http://download.java.net/openjdk/jdk7/

特定の開発バージョンのソースコードを入手したい場合は「Mercurial」*2を使います。Mercurial

とは Pythonで作られたフリーの分散バージョン管理システムです。

OpenJDK 内には複数のプロジェクトがあり、それにともなって複数のリポジトリがあります。

本章では HotspotVM のソースコードのみ必要となりますので、Mercurial を使って HotspotVM

プロジェクトのリポジトリからソースコードを入手しましょう。次のコマンドを入力すると、リポ

ジトリからコードツリーをチェックアウトできます。

hg clone -r jdk7-b147 http://hg.openjdk.java.net/jdk7/jdk7/hotspot hotspot

1.4 ソース構成

HotspotVMのソースコードを入手したら、その中に「src」という名前のディレクトリがあると

おもいます。そこに HotspotVMのソースコードが置かれています。

表 1.1 ディレクトリ構成

ディレクトリ 概要

cpu CPU依存コード群os OS依存コード群os_cpu OS、CPU依存コード群(Linuxでかつ x86等)share 共通コード群

表 1.1内最後の「share」ディレクトリ内には「vm」というディレクトリがあります。この「vm」

ディレクトリの中に HotspotVMの大半部分のコードが置かれています。

また、「src」ディレクトリ内のソースコード分布を表 1.2に示します。

表 1.2 ソースコード分布

言語 ソースコード行数 割合

C++ 420,791 93%Java 21,231 5%C 7,432 2%

HotspotVMは約 45万行のソースコードから成り立っており、そのほとんどが C++で書かれて

います。

1.5 G1GCのアルゴリズム

HotspotVMには次の GCアルゴリズムが実装されています。

*2 Mercurial公式サイト:http://mercurial.selenic.com/wiki/

3

Page 10: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 1章 準備 1.5. G1GCのアルゴリズム

• 世代別 GC

• 並列世代別 GC

• 並行マークスイープ GC

• G1GC

本書では OpenJDK7から導入された G1GCについて説明します。

4

Page 11: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

5

第 2章

アロケータ

この章では HotspotVMのアロケーションについて詳しく説明します。

2.1 アロケーションの流れ

G1GCにおけるオブジェクトのアロケーションを VMヒープの初期化から順を追ってみていきま

しょう。ここでは実装の詳細ではなく、抽象的に HotspotVMが VMヒープをどのようにアロケー

ションしているか、その概念を説明します。

図 2.1 (1)VMヒープの予約

まず、G1GCの VMヒープ(G1GCヒープとパーマネント領域)の最大サイズ分をメモリ領域に

「予約」します。最大 G1GCヒープサイズと最大パーマネント領域サイズは言語利用者が指定でき

ます。指定されなかった場合、標準の VMヒープの最大サイズは OSによっても異なりますが、ほ

とんどの場合、G1GCヒープの最大サイズが 64Mバイト、パーマネント領域の最大サイズが 64M

バイトの合わせて 128Mバイトととなります。ここではメモリ領域を「予約」するだけで、実際に

物理メモリに割り当てられません。また、G1GCの VMヒープはリージョンのサイズでアラインメ

ントされます。

Page 12: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.1. アロケーションの流れ

図 2.2 (2)VMヒープの確保

次に、予約しておいた VMヒープに必要最小限のメモリ領域を「確保」します。ここで実際に物

理メモリへ割り当てされます。G1GCヒープの方はリージョン単位で「確保」されます。

図 2.3 (3)オブジェクトのアロケーション

ここからはパーマネント領域のアロケーションについては説明から除外し、G1GCヒープ内への

アロケーションのみを見ていきましょう。G1GC ヒープにはリージョンが確保されました。その

リージョンに対してオブジェクトがアロケーションされます。

6

Page 13: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.2. VMヒープの予約

図 2.4 (4)G1GCヒープの拡張

オブジェクトのアロケーションによってリージョンが枯渇すると、予約しておいたメモリ領域か

らメモリを「確保」し、新たにリージョンを 1個割り当て、G1GCヒープを拡張します。そして、割

り当てたリージョンの中にオブジェクトをアロケーションします。

2.2 VMヒープの予約

では、実際にどのように実装されているかを見ていきましょう。

VMヒープの予約は最初に G1GCヒープの初期化処理を行う initialize()メンバ関数に書か

れています。その処理部分だけを次に抜き出しました。

share/vm/gc_implementation/g1/g1CollectedHeap.cpp1794: jint G1CollectedHeap::initialize() {

1810: size_t max_byte_size = collector_policy()->max_heap_byte_size();

1819: PermanentGenerationSpec* pgs = collector_policy()->permanent_generation();

1825: ReservedSpace heap_rs(max_byte_size + pgs->max_size(),

1826: HeapRegion::GrainBytes,

1827: UseLargePages, addr);

1810行目の collector_policy()メンバ関数はG1GCに関するフラグや設定値等が定義されて

いる G1CollectorPolicyクラスのインスタンスへのポインタを返し、max_heap_byte_size()メ

ンバ関数は名前の通り、最大 G1GCヒープサイズを返します。したがって、max_byte_sizeロー

カル変数には最大 G1GCヒープサイズが格納されます。

1819行目の pgsにはパーマネント領域に関する設定値等が定義されている PermanentGenerationSpec

クラスのインスタンスへのポインタが格納されます。

1825行目で ReservedSpaceクラスのインスタンスを生成しています。この際に実際に VMヒー

プを予約しています。ReservedSpace クラスのインスタンス生成には次の引数を渡します。1827

7

Page 14: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.3. VMヒープの確保

行目の他の引数(UseLargePages、addr)については使用されませんので無視してください。

1. 最大 G1GCヒープサイズ + 最大パーム領域サイズ

2. リージョンサイズ(HeapRegion::GrainBytes)

1. は予約するメモリ領域のサイズです。2.はメモリ領域のアラインメントに使います。

肝心の ReservedSpaceクラスの定義は次の通りです。

share/vm/runtime/virtualspace.hpp32: class ReservedSpace VALUE_OBJ_CLASS_SPEC {

33: friend class VMStructs;

34: private:

35: char* _base;

36: size_t _size;

38: size_t _alignment;

35行目の_baseメンバ変数には予約したメモリ領域の先頭アドレスが格納されます。_sizeには

メモリ領域のサイズ、_alignmentにはメモリ領域がアラインメントされた値がそれぞれ格納され

ます。

ここでは実装の詳細は書きませんが、今の段階ではこの ReservedSpace クラスを生成するとメ

モリ領域が予約されると考えてもらえばよいでしょう。

share/vm/gc_implementation/g1/g1CollectedHeap.cpp1794: jint G1CollectedHeap::initialize() {

/* 省略:ReservedSpace生成 */

1884: ReservedSpace g1_rs = heap_rs.first_part(max_byte_size);

1889: ReservedSpace perm_gen_rs = heap_rs.last_part(max_byte_size);

ReservedSpace クラスのインスタンスを生成し終わると、G1GC ヒープとパーマネント領域を

分割して、それぞれに対応したローカル変数(g1_rs、perm_gen_rs)に格納します。

2.3 VMヒープの確保

予約した VM ヒープ用のメモリ領域を実際に「確保」していくクラスが VirtualSpace クラス

です。

share/vm/gc_implementation/g1/g1CollectedHeap.hpp143: class G1CollectedHeap : public SharedHeap {

176: VirtualSpace _g1_storage;

8

Page 15: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.3. VMヒープの確保

G1CollectedHeapクラスには VirtualSpaceクラスのインスタンスをもつメンバ変数が定義さ

れています(ポインタではないことに注意してください)。

share/vm/gc_implementation/g1/g1CollectedHeap.cpp1794: jint G1CollectedHeap::initialize() {

/* 省略:G1GCヒープ用メモリ領域の予約 */

1891: _g1_storage.initialize(g1_rs, 0);

1891 行目で_g1_storage メンバ変数を初期化します。第 1 引数に生成した G1GC ヒープ用の

ReservedSpaceクラスのインスタンスへのポインタを渡し、第 2引数には確保するサイズを指定し

ます。この場合は 0です。したがって、まだメモリ領域は確保されていません。

では、実際に確保する処理を見てみましょう。

share/vm/gc_implementation/g1/g1CollectedHeap.cpp1794: jint G1CollectedHeap::initialize() {

1809: size_t init_byte_size = collector_policy()->initial_heap_byte_size();

/* 省略:G1GCヒープ用メモリ領域の予約 */

1937: if (!expand(init_byte_size)) {

1809 行目で init_byte_size にはじめに確保するメモリ領域サイズを格納します。そして

expand()メンバ関数内でメモリ領域を確保します。

share/vm/gc_implementation/g1/g1CollectedHeap.cpp1599: bool G1CollectedHeap::expand(size_t expand_bytes) {

/* 省略:expand_bytesをリージョンサイズで切り上げ */

1610: HeapWord* old_end = (HeapWord*)_g1_storage.high();

1611: bool successful = _g1_storage.expand_by(aligned_expand_bytes);

1612: if (successful) {

1613: HeapWord* new_end = (HeapWord*)_g1_storage.high();

1624: expand_bytes = aligned_expand_bytes;

1625: HeapWord* base = old_end;

1626:

1627: // old_endから new_endCreateまでのヒープリージョン作成1628: while (expand_bytes > 0) {

1629: HeapWord* high = base + HeapRegion::GrainWords;

1630:

1631: // リージョン生成1632: MemRegion mr(base, high);

9

Page 16: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.4. WINDOWSでのメモリ領域の予約、確保

1634: HeapRegion* hr = new HeapRegion(_bot_shared, mr, is_zeroed);

1635:

1636: // HeapRegionSeqに追加1637: _hrs->insert(hr);

1643: expand_bytes -= HeapRegion::GrainBytes;

1644: base += HeapRegion::GrainWords;

1645: }

1667: return successful;

1668: }

前半部分で引数に受け取った expand_bytesをリージョンサイズで切り上げ、aligned_expand_bytes

に設定します。

1610行目で確保しているメモリ領域の終端を受け取ります。今回の場合、メモリ領域はまだ確保

されていませんので、予約された VMヒープ用メモリ領域の先頭アドレスが戻ります。また、同行

に登場する HeapWord*は VMヒープ内のアドレスを指す場合に使用します。

1611行目にある VirtualSpaceクラスの expand_by()メンバ関数で実際のメモリ領域の確保を

行います。リージョン 1個分を確保しています。

メモリ領域の確保に成功した場合は、その領域を管理するリージョンを生成します。

1629行目で baseのリージョン 1個分先のアドレスを設定します。1632行目の MemRegionクラ

スはアドレスの範囲を管理するクラスです。コンストラクタの引数には範囲の先頭アドレスと終端

アドレスを渡します。そして、1634行目で HeapRegionクラスのインスタンスを生成します。第 1

引数の_bot_sharedと第 3引数の is_zeroedは特に関係ありませんので、ここでは無視します。

1637行目で生成した HeapRegionクラスのインスタンスへのポインタを HeapRegionSeqに追加

すれば、リージョン 1個分のメモリ領域確保は終了です。あとはこれを expand_bytesの分繰り返

すだけです。

2.4 Windowsでのメモリ領域の予約、確保

メモリ領域の予約、確保は実際どのように実装されているのか調べていきましょう。実装方法は

OSによって異なります。まずはWindowsから見ていきましょう。

Windows には VirtualAlloc() というWindowsAPI があります。HotspotVM ではこの API

を使ってメモリ確保の予約、確保を実現しています。

VirtualAlloc()は仮想アドレス空間内のページ領域を、予約またはコミットする APIです。引

数には次の情報を渡します。

1. 確保、また予約したいメモリ領域の開始アドレス。NULLの場合、システムがメモリ領域の

開始アドレスを決定

2. サイズ

3. 割り当てのタイプ

4. アクセス保護のタイプ

10

Page 17: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.5. LINUXでのメモリ領域の予約、確保

では、実際にメモリ領域を予約している os::reserve_memory()メンバ関数を見てみましょう。

os/windows/vm/os_windows.cpp2717: char* os::reserve_memory(size_t bytes, char* addr, size_t alignment_hint) {

2721: char* res = (char*)VirtualAlloc(addr, bytes, MEM_RESERVE, PAGE_READWRITE);

2724: return res;

2725: }

2721行目の第 3引数に渡されている MEM_RESERVEがメモリ領域を「予約」する際のフラグです。

MEM_RESERVEが渡されると指定されたサイズのメモリ領域は予約されるだけで実際には物理メモリ

に割り当てされません。

次に、メモリ領域を確保する os::commit_memory()メンバ関数を見てみましょう。不要な部分

は省略しました。

os/windows/vm/os_windows.cpp2857: bool os::commit_memory(char* addr, size_t bytes, bool exec) {

2866: bool result = VirtualAlloc(addr, bytes, MEM_COMMIT, PAGE_READWRITE) != 0;

2872: return result;

2874: }

2866行目の第 3引数に渡されている MEM_COMMITがメモリ領域を「確保」する際のフラグです。

MEM_COMMIT が渡されると指定されたサイズ分のメモリ領域が実際に物理メモリと割り当てられ

ます。

2.5 Linuxでのメモリ領域の予約、確保

Linuxではメモリ領域の「予約」「確保」を mmap()で実装しています。

実は Linuxにはメモリ領域を「予約」するという概念はありません。mmap()するとメモリ領域は

「確保」されます。ただし、メモリ領域は「確保」されても物理メモリが割り当てられるわけではあ

りません。物理メモリが割り当てられるのは確保したメモリ領域に実際にアクセスされたときです。

「デマンドページング」と呼ばれる機能です。

メモリ領域の「予約」にあたる部分である os::reserve_memory()メンバ関数の Linux版を見

ていきましょう。

os/linux/vm/os_linux.cpp2787: char* os::reserve_memory(size_t bytes, char* requested_addr,

2788: size_t alignment_hint) {

2789: return anon_mmap(requested_addr, bytes, (requested_addr != NULL));

2790: }

11

Page 18: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.6. VMヒープのアラインメント実現方法

2751: static char* anon_mmap(char* requested_addr, size_t bytes, bool fixed) {

2752: char * addr;

2753: int flags;

2754:

2755: flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS;

2756: if (fixed) {

2758: flags |= MAP_FIXED;

2759: }

2763: addr = (char*)::mmap(requested_addr, bytes, PROT_READ|PROT_WRITE,

2764: flags, -1, 0);

2776: return addr == MAP_FAILED ? NULL : addr;

2777: }

os::reserve_memory() は内部で os::anon_mmap() を呼び出すだけです。os::anon_mmap()

は MAP_ANONYMOUSを使ってメモリ領域を確保します。Linux版ではメモリ領域を「予約」するので

はなく、実際には「確保」するのですね。

また、2751行目で mmap()に渡される flagローカル変数に設定している MAP_NORESERVEには「ス

ワップ空間の予約を行わない」という意味があります。mmap()してアドレスが確保された場合、そ

のメモリ領域に確実に割り当てられる保証を得るために、スワップ空間をサイズ分一気に予約してし

まう OS があります。MAP_NORESERVE にはそれを防ぐ効果があります。os::reserve_memory()

の段階では VMヒープ用のメモリ領域を「予約」するだけで、実際にオブジェクトをアロケーショ

ンするなどのアクセスを行いません。そのため、スワップ空間を予約するのは無駄だということで

す。HP-UX*1のようにスワップ空間を予約する OSには効果のある工夫です。

メモリ領域の「確保」にあたる部分である os::commit_memory()メンバ関数では、逆に「確保」

したいアドレス分だけ MAP_NORESERVEを付けずに mmap()します。似たような処理のため、コード

の紹介はしません。

Linuxの場合、実際に物理メモリに割り当てられタイミングはWindowsとは異なります。確保し

たメモリ領域にオブジェクトがアロケーションされ、実際にアクセスされたときに物理メモリが割

り当てられます。

2.6 VMヒープのアラインメント実現方法

VMヒープはリージョンのサイズでアラインメントされています。つまり、VMヒープの先頭ア

ドレスはリージョンサイズの倍数になっているということです。HotspotVM ではこのアラインメ

ントをどのようにして実現しているのでしょうか?

実装方法は拍子抜けするほどシンプルです。具体的には次の手順で処理を行います。ここでは説

明を簡単にするため、アライメントサイズ(リージョンサイズ)を 1Kバイト、VMヒープのサイズ

は 1Kバイトよりも大きいと仮定します。

1. VMヒープサイズ分のメモリ領域を予約

*1 HP-UX:ヒューレット・パッカード社(HP 社)製の UNIX オペレーティングシステム

12

Page 19: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.6. VMヒープのアラインメント実現方法

2. 帰ってきたメモリ領域の範囲に 1Kバイトの倍数アドレスを保持

3. 1.で確保したメモリ領域の予約を破棄

4. 2.で保持しておいたアドレスを指定し、再度 VMヒープサイズ分のメモリ領域を予約

5. 4.に失敗した場合は 1.に戻る

13

Page 20: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.6. VMヒープのアラインメント実現方法

図 2.5 アラインメントされた VMヒープの予約

まず、1. で VM ヒープサイズ分のメモリ領域を予約してしまいます。メモリ領域の予約には

os::reserve_memopry()関数を使います。

予約したメモリ領域の範囲内には 1Kバイトの倍数アドレスがどこかにはあるはずです。1Kバイ

ト以上のメモリ領域を予約しているのですから当たり前ですね。その 1Kバイトの倍数アドレスが、

14

Page 21: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.7. オブジェクトのアロケーション

アラインメントされたアドレスになります。ここで重要なのは上記で取得したアラインメントされ

たアドレスが、OSが「このメモリ領域は使えるよ」と返してきたものだということです。2.ではそ

のアドレスを保持しておきます。

3.では 1.で予約したメモリ領域を一度破棄してしまいます。メモリ領域は予約しているだけであ

り、実メモリには割り当てられてはいませんので、破棄にかかるコストは微々たるものです。

4. では 2. で保持しておいたアドレスを指定して、VM ヒープサイズ分のメモリ領域を予約しま

す。メモリ領域の予約を実際に行う mmap()や VirtualAlloc()は予約するメモリ領域の先頭アド

レスを指定することができますので、これを利用します。

ただし、2.の段階では使ってよかったはずのアドレスが、4.の段階で使えなくなっていることも

考えられます。そのため、5.によって成功するまで繰り返す処理を繰り返します。

2.7 オブジェクトのアロケーション

無事、リージョンはメモリ確保されました。では、そのリージョンからオブジェクトをアロケー

ションする部分を見ていきましょう。

HeapRegion クラスの親クラスである ContiguousSpace クラスの allocate() メンバ関数に

よってリージョンからメモリ領域を確保します。

share/vm/memory/space.cpp827: inline HeapWord* ContiguousSpace::allocate_impl(size_t size,

828: HeapWord* const end_value) {

838: HeapWord* obj = top();

839: if (pointer_delta(end_value, obj) >= size) {

840: HeapWord* new_top = obj + size;

841: set_top(new_top);

843: return obj;

844: } else {

845: return NULL;

846: }

847: }

allocate()では単純にリージョン内のチャンクの先頭を指す_topをオブジェクトのサイズの分

ずらすだけです。

関数の引数である sizeにはオブジェクトサイズのバイト数ではなく、ワード数が渡されます。も

う 1つの引数、end_valueにはリージョン内チャンクの終端アドレスが渡されます。

839 行目の pointer_delta() は指定されたアドレスの差分をワード数で戻す関数です。もし、

size分空きがなければ 845行目で NULLを戻します。

チャンクに空きがあれば、840行目で objを size分ずらして、841行目でチャンクの先頭アドレ

スに設定します。そして、確保したメモリ領域の先頭(obj)を 843行目で戻します。

15

Page 22: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.8. 大型オブジェクトのアロケーション

2.8 大型オブジェクトのアロケーション

G1GCではリージョンのサイズの半分を超えるオブジェクトを大型(humongous)オブジェクト

と呼びます。この大型オブジェクトのアロケーションはどのように行われるでしょうか?

リージョン 1 個半のサイズをもつ大型オブジェクトをアロケーションしたいと仮定します。ま

ず、G1GCのアロケータは HeapRegionSeqクラスのインスタンスが持つ HeapRegionの配列を走

査し、2個連続した空のリージョンを探します。

2個の空リージョンそれぞれの allocation()関数をリージョンサイズを引数に呼び出し、リー

ジョン全体をアロケーションします。つまり、大型オブジェクトのサイズがリージョン 1個半でも、

アロケータは 2個分のリージョンを確保します。

2.9 TLAB(Thread Local Allocation Buffer)

VMヒープはすべてのスレッドの共有領域です。そのため、VMヒープからオブジェクトをアロ

ケーションする際には VMヒープをロックし、他のスレッドからのアロケーションが割り込まない

ようにする必要があります。

しかし、せっかく別々の CPUコアで動作していたスレッドをアロケーション時にいちいちロック

してしまうのは嬉しくありません。その問題を解決するためにそれぞれのスレッド専用のアロケー

ション用バッファを持たせてロックの回数を少なくしよう、というのが TLABの考えです。

あるスレッドの最初のオブジェクトアロケーション時に、一定サイズのメモリ領域を VMヒープ

からアロケーションし、スレッド内にバッファとして貯め込みます。このバッファを TLABと呼び

ます。VMヒープのロックが必要なのは、TLABを確保するときのみです。

同スレッドからの次のオブジェクトアロケーション時には、TLAB内からオブジェクトサイズ分

アロケーションします。この時は他スレッドからアクセスされる可能性がないため、VMヒープの

ロックは必要ありません。

図 2.6 TLABによるアロケーション

TLABは通常はオフになっていますが、言語利用者が Javaの起動オプションによってオンにで

16

Page 23: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 2章 アロケータ 2.9. TLAB(THREAD LOCAL ALLOCATION BUFFER)

きます。さらに TLABのサイズも指定可能です。

17

Page 24: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

18

第 3章

ヒープ構造

この章では HotspotVMはどのようなヒープ構造をもっているか、その全体像を探っていきます。

3.1 VMヒープ

HotspotVMの VMヒープは大きく次の 2つに分かれます。

1. 選択した GC用のメモリ領域

2. パーマネント(Permanent)領域

図 3.1 VMヒープの全体像

Java(HotspotVM)の利用者は GCアルゴリズムを選択することができました。HotspotVMは

言語利用者によって GCアルゴリズムが選択されると、その GC用の構造を持つメモリ領域を作成

します。それが 1.です。このメモリ領域に選択した GCの対象オブジェクトを割り当てます。

2.のパーマネント領域のパーマネント(Permanent)には「永続的な」という意味があります。こ

の名前の通り、パーマネント領域には型情報(klassOop)やメソッド情報(methodOop)等の長生

きするオブジェクトが割り当てられます。パーマネント領域は GCアルゴリズムが変更されてもほ

ぼ同じ構造で VMヒープの一部として確保されます。

Page 25: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.2. CHEAPOBJクラス

3.2 CHeapObjクラス

VM ヒープの実装を見る前に CHeapObj というクラスについて説明しておきましょう。

HotspotVM内の多くのクラスはこの CHeapObjクラスを継承しています。

CHeapObjクラスの特殊な点はオペレータの newと deleteを書き換え、C++の通常のアロケー

ションにデバッグ処理を追加してるところです。このデバッグ処理は開発時にのみ使用されます。

CHeapObj クラスにはいくつかのデバッグ処理が実装されていますが、その中で「メモリ破壊検

知」について簡単に見てみましょう。

デバッグ時、CHeapObjクラス(または継承先クラス)のインスタンスを生成する際に、わざと余

分にアロケーションします。アロケーションされたメモリのイメージを図 3.2に示します。

図 3.2 デバッグ時の CHeapObjインスタンス

余分にアロケーションしたメモリ領域を図 3.2内の「メモリ破壊検知領域」として使います。メ

モリ破壊検知領域には 0xABという値を書き込んでおきます。CHeapObjクラスのインスタンスとし

て図 3.2内の真ん中のメモリ領域を使用します。

そして、CHeapObjクラスのインスタンスの delete時に「メモリ破壊検知領域」が 0xABのまま

であるかをチェックします。もし、書き換わっていれば、CHeapObjクラスのインスタンスの範囲を

超えたメモリ領域に書き込みが行われたということです。これはメモリ破壊が行われた証拠となり

19

Page 26: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.3. COLLECTEDHEAPクラス

ますので、発見次第エラーを出力し、終了します。

3.3 CollectedHeapクラス

VMヒープは CollectedHeapという抽象的なクラスで統一的に扱われます。CollectedHeapク

ラスは GCアルゴリズムによってサブクラスに派生し、このサブクラスが VMヒープの実体となり

ます。また、CollectedHeapクラスは CHeapObjクラスを継承しています。

図 3.3に CollectedHeapクラスの継承関係を示しています。

図 3.3 CollectedHeapクラスの継承関係

本章では G1GC用の VMヒープである g1CollectedHeapクラスについてのみ説明します。

3.4 G1GCヒープ

G1GCのヒープは一定のサイズのリージョンに区切られています。ここではG1GCヒープがリー

ジョンをどのように保持しているかを見ていきましょう。

G1GCヒープはそのサイズ分のメモリ領域を一度に確保します。そのため、G1GCヒープ内に確

保されるリージョンは連続したアドレスになります。

そして、リージョンには、それぞれを管理する HeapRegionクラスのインスタンスが存在します。

これを HeapRegionと呼ぶことにします。

HeapRegion は_bottom、_end メンバ変数をもち、それぞれ G1GC ヒープ内のリージョンの先

頭アドレス、終端アドレスを格納しています。

20

Page 27: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.4. G1GCヒープ

図 3.4 HeapRegion

次に列挙するのは g1CollectedHeapクラスの主要な 3つのメンバ変数とその役割です。

• _hrs - すべての HeapRegionを配列によって保持

• _young_list - 新世代の HeapRegionリスト

• _free_region_list - 未使用の HeapRegionリスト

図 3.5 G1GCヒープの構造

g1CollectedHeap クラスは HeapRegionSeq クラスのインスタンスへのポインタを格納する

_hrs メンバ変数を持ちます。HeapRegionSeq クラスの_regions メンバ変数という配列には

21

Page 28: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.5. HEAPREGIONSEQクラス

HeapRegionクラスのインスタンス(以下、HeapRegion)のアドレスが格納されています。

HeapRegionは g1CollectedHeapクラスから伸びる_young_list、_free_region_listによっ

て片方向リストでつながれています。

新世代の HeapRegion は_young_list につながれています。空のリージョンと対応する

HeapRegionはフリーリージョンリスト(_free_region_list)によってつながれています。そし

て、旧世代の HeapRegionは何のリンクにもつながれていません。

3.5 HeapRegionSeqクラス

HeapRegionSeqクラスは HotspotVMが独自に実装している GrowableArrayという配列を表現

するクラスをラップする形で定義されています。

share/vm/gc_implementation/g1/heapRegionSeq.hpp34: class HeapRegionSeq: public CHeapObj {

35:

38: GrowableArray<HeapRegion*> _regions;

56: public:

63: void insert(HeapRegion* hr);

70: HeapRegion* at(size_t i) { return _regions.at((int)i); }

114: };

38 行目の_regions メンバ変数がリージョンに対応するを保持する配列(GrowableArray クラ

ス)です。

GrowableArrayクラスは通常の配列と違い、要素を追加する際に配列を拡張する処理が実装され

ています。名前の通り、増大可能な(Growable)配列なのです。

_regionsメンバ変数の配列内の HeapRegionはリージョンのアドレスによって昇順にソートさ

れています。つまり、インデックス 0 とインデックス 1 に格納されている HeapRegion は、VM

ヒープ上で隣り合ったリージョンを管理しているということです。

63行目の insert()メンバ関数で_regionsに新しいリージョンのアドレスを追加します。この

insert()関数内で配列のソート処理を行います。

70行目の at()メンバ関数は指定したインデックスのリージョンを返します。

3.6 HeapRegionクラス

リージョンを管理する HeapRegionクラスをさらに詳しく見ていきましょう。

HeapRegionクラスの継承図を図 3.6に示します。

22

Page 29: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.6. HEAPREGIONクラス

図 3.6 HeapRegion継承図

継承関係が深いですが、そのすべてを覚える必要はありません。「様々なクラスから機能を受け継

いでいる」ということがわかれば OKです。

HeapRegionクラスには_bottom、_top、_endという 3つのローカル変数があります。それぞれ

の意味は次の通りです。

• _bottom - リージョンの先頭アドレス

• _top - リージョン内のチャンク先頭アドレス

• _end - リージョンの終端アドレス

この 3つのローカル変数は Spaceクラスに定義されています。もっともよく登場するローカル変

数ですので、きちんと頭に入れておいてください。

さらに、HeapRegionクラスには 3つの片方向リスト用のメンバ変数が定義されています。

1. _next_young_region

2. _next_dirty_cards_region

3. _next_in_special_set

1.は名前の通り、次の新世代リージョンを指します。

もっとも重要なのは 3.の_next_in_special_setメンバ変数です。このメンバ変数はリージョン

が所属する集合によって意味の違う様々なリージョンを指します。具体的に言えば、リージョンがフ

リーリージョンリストに所属するときには、次の空リージョンがつながれ、GC対象のリストに所属

するときには、次の GC対象である使用中のリージョンがつながれます。_next_in_special_set

23

Page 30: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 3章 ヒープ構造 3.7. パーマネント領域

メンバ変数は用途によって様々な使い方がされるということを覚えておいてください。

また、_next_in_special_setメンバ変数にリージョンをつなぐとき、_next_in_special_set

メンバ変数が何の用途に使われているかを覚えておくため、「このリージョンはこの集合に所属して

います」というフラグを立てておきます。

フラグに使用するメンバ変数は次の通りです。これらのフラグはすべて bool型です。

• _in_collection_set - GC対象のリージョン

• _is_on_unclean_list - 空のリージョン(0クリアがまだ)

• _is_on_free_list - 空のリージョン(0クリアが済み)

• _is_gc_alloc_region - GC時にオブジェクトを退避するリージョン

ここではすべてを覚える必要はありません。このようなものでリージョンの所属を判断している

のかという点が理解できれば十分です。

3.7 パーマネント領域

G1GCのパーマネント領域は CompactingPermGenGenクラスによって管理されています。

g1CollectedHeap クラスの_perm_gen というメンバ変数に CompactingPermGenGen クラスの

インスタンスへのポインタが格納されます。

パーマネント領域は G1GCではなく、マークコンパクト GCの対象となります。そのため、本章

では詳細な説明は行いません。

24

Page 31: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

25

第 4章

オブジェクト構造

まずは、データ構造から見ていきましょう。

4.1 oopDescクラス

oopDescクラスは GC対象となるオブジェクトの抽象的な基底クラスです。oopDescクラスを継

承したクラスのインスタンスが GC対象のオブジェクトとなります。

oopDescクラスの継承関係を図 4.1に示します。

図 4.1 oopDescクラスの継承関係

oopDescクラスには次のメンバ変数が定義されています。

share/vm/oops/oop.hpp61: class oopDesc {

63: private:

64: volatile markOop _mark;

65: union _metadata {

66: wideKlassOop _klass;

67: narrowOop _compressed_klass;

68: } _metadata;

69:

71: static BarrierSet* _bs;

Page 32: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 4章 オブジェクト構造 4.2. KLASSOOPDESCクラス

64 行目の_mark 変数はオブジェクトのヘッダとなる部分です。_mark 変数にはマークスイープ

GC 用のマークはもちろんのこと、その他のオブジェクトに必要な様々な情報が詰め込まれてい

ます。

oopDesc は自分のクラスへのポインタを保持しています。それが 65 行目に共用体で定義されて

いる_metadata変数です。この共用体にはほとんどの場合、66行目の_klass変数の値が格納され

ます。_klass変数はその名前の通りオブジェクトのクラスへのポインタを格納します。67行目の

_compressed_klassは本章では GCとは関係ないため特に触れません。

HotspotVMでは oopDescクラスやそのサブクラスのインスタンスへのポインタ(oopDesc*)を

typedefで別名定義しています。

share/vm/oops/oopsHierarchy.hpp42: typedef class oopDesc* oop;

43: typedef class instanceOopDesc* instanceOop;

44: typedef class methodOopDesc* methodOop;

45: typedef class constMethodOopDesc* constMethodOop;

46: typedef class methodDataOopDesc* methodDataOop;

47: typedef class arrayOopDesc* arrayOop;

48: typedef class objArrayOopDesc* objArrayOop;

49: typedef class typeArrayOopDesc* typeArrayOop;

50: typedef class constantPoolOopDesc* constantPoolOop;

51: typedef class constantPoolCacheOopDesc* constantPoolCacheOop;

52: typedef class klassOopDesc* klassOop;

53: typedef class markOopDesc* markOop;

54: typedef class compiledICHolderOopDesc* compiledICHolderOop;

すべて Descを取り除いた名前に別名定義されています。oopDescの Descは「Describe(表現)」

の略です。つまり、oopDescとは oopという実体(オブジェクト)をクラスとして「表現」してい

るものなのです。oop等の別名定義は今後、頻繁に登場するため意味も含めてしっかりと抑えてお

いてください。本章では oopDesc等のインスタンスを別名定義のルールに従って oopのように呼ぶ

ことにします。

4.2 klassOopDescクラス

oopDescクラスを継承している klassOopDescクラスは Java上のクラスを表すクラスです。つ

まり、Java上の「java.lang.String」は、VM上では klassOopDescクラスのインスタンス(klassOop)

となります。クラス名の一部が「class」ではなく「klass」となっているのは、C++の予約語と

区別するためです。このテクニックは多くの言語処理系によく見かけます。

前の「4.1 oopDescクラス」の項で説明した通り、全オブジェクトは klassOopを持っています。

klassOopDescも oopDescを継承しているため、klassOopを持っています。

klassOop の特徴は内部に Klass クラスのインスタンスを保持しているということです。実は

klassOopDescクラス自体に情報はほとんどなく、klassOopは内部に Klassクラスのインスタン

スを保持するただの箱にすぎません。

26

Page 33: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 4章 オブジェクト構造 4.3. KLASSクラス

4.3 Klassクラス

Klassクラスは名前の通り、型情報を保持しています。Klassクラスのインスタンスは klassOop

の一部として生成されます。

Klassクラスは様々な型情報の抽象的な基底クラスです。Klassクラスの継承関係を図 4.2に示

します。

図 4.2 Klassクラスの継承関係

Klassクラスのサブクラスには、oopDescのサブクラスと対応するクラスが存在します。oopDesc

のサブクラスである XXDescのインスタンスがもつ klassOop内には、XXDescに対応した XXKlass

のインスタンスが格納さているということです。

前の「4.2 klassOopDescクラス」の項で klassOopはただの箱だといいました。klassOopはオ

ブジェクトとして Klassやそのサブクラスを統一的にあつかうためのインタフェースだといえるで

しょう。つまり、外側は klassOopだとしても内部には instanceKlassや symbolKlass等がなど

が入っているということです(図 4.3)。

図 4.3 klassOopは Klassの箱

4.4 クラスの関係

では、1つのオブジェクトを例にとって oopと Klassの関係を見ていきましょう。

次のような Stringクラスのオブジェクトを生成する Javaプログラムがあったとします。

27

Page 34: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 4章 オブジェクト構造 4.5. OOPDESCクラスに仮想関数を定義してはいけない

1: String str = new String();

2: System.out.println(str.getClass()); // => java.lang.String

3: System.out.println(str.getClass().getClass()); // => java.lang.Class

4: System.out.println(str.getClass().getClass().getClass()); // => java.lang.Class

その場合、str変数には Stringクラスのオブジェクトが格納されます。この時、HotspotVM上

での oopと Klassの関係は図 4.4のようになっています。

図 4.4 Stringオブジェクトの oop

instanceOopは Java上のインスタンスと同じ意味を持ちます。つまり、「new String()」を VM

で評価すると instanceOopが 1つ生成されます。

instanceOopのクラスはもちろん klassOopです。そして、klassOopの中には instanceKlass

のインスタンスが格納されています。この klassOop は Java 上の String クラスと対応してい

ます。

次に、Java 上の String クラスの klassOop のクラスは同じく klassOop です。この klassOop

の中には instanceKlassKlassのインスタンスが格納されています。

instanceKlassKlass は instanceKlass のクラスです。instanceKlassKlass をもつ

klassOop のクラスは自分自身であるため、instanceOop から続くクラスの連鎖を止める

役割を持っています。Java プログラムを見ると 3 行目の getClass() メソッドの結果と、

4 行目の getClass() メソッドの結果が同じ値になっています。これはクラスの連鎖が

instanceKlassKlassのところでループしているためです。

4.5 oopDescクラスに仮想関数を定義してはいけない

oopDescクラスには C++の仮想関数(virtualfunction)*1を定義してはいけない決まりになって

います。

その理由はクラスに仮想関数を定義すると C++のコンパイラがそのクラスのインスタンスに仮

想関数テーブル*2へのポインタを付けてしまうからです。すべてのオブジェクトに 1ワード確保さ

れては困りものです。そのため、oopDesc クラスには C++ の仮想関数を定義できないルールと

*1 仮想関数:サブクラスで再定義可能な関数のこと。C++上の文法でメンバ関数に virtual を付けると仮想関数となる*2 仮想関数テーブル:実行時に呼び出すメンバ関数の情報を格納している

28

Page 35: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 4章 オブジェクト構造 4.5. OOPDESCクラスに仮想関数を定義してはいけない

なっています。

もし、仮想関数を使ってサブクラス毎に違う振る舞いをするメンバ関数を定義したい場合は

oopDescではなく、対応する Klassの方に仮想関数を定義します。

次にその一部を示します。ここでは自分が Java上でどのような意味をもつオブジェクトかを判断

する仮想関数が定義されています。

share/vm/oops/klass.hpp172: class Klass : public Klass_vtbl {

// Java上の配列か?582: virtual bool oop_is_array() const { return false; }

上記のメンバ関数は Klassを継承するクラスで次のように再定義されます。

shara/vm/oops/arrayKlass.hpp35: class arrayKlass: public Klass {

47: bool oop_is_array() const { return true; }

そして、oopDescクラスでは Klassクラスの仮想関数を呼び出します。

share/vm/oops/oop.inline.hpp139: inline Klass* oopDesc::blueprint() const {

return klass()->klass_part(); }

146: inline bool oopDesc::is_array() const {

return blueprint()->oop_is_array(); }

139 行目の blueprint() は klassOop の中から Klass のインスタンスを取り出すメンバ関

数です。146 行目では Klass の仮想関数で定義されたメンバ関数を呼び出しています。oop の

is_array() を呼び出しても、対応した Klass の oop_is_array() が応答し、false が戻ります

が、arrayOop の is_array() を呼び出すと、対応した arrayKlass の oop_is_array() が応答

し、trueが戻ります。

Klass クラスに仮想関数を定義するため、klassOop には仮想関数テーブルへのポインタが付い

てしまいますが、Java上のクラスは全体量が少ないため、それほどメモリを消費しません。

29

Page 36: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

30

第 5章

正確な GCへの道(HotspotVM編)

この章では HotspotVMが正確な GCのためにどのような工夫をしているかを見ていきます。

5.1 プリミティブ型と参照型の変数

リスト 5.1: プリミティブ型と参照型

1: int primitiveType = 1; // プリミティブ型2: Object referenceType = new Object(); // 参照型

Java の変数に格納される型として int、float といった「プリミティブ型」があります (リス

ト 5.1 の 1 行目)。プリミティブ型は Java 上では数値として扱われます。それと同様に C++

(HotpostVM)上でも intや floatといった数値として扱われます。

一方、Objectクラス(またはそのサブクラス)のインスタンスを指す「参照型」があります (リ

スト 5.1の 2行目)。参照型は C++(HotspotVM)上ではオブジェクトへのポインタとして扱われ

ます。

ここで問題となるのが「プリミティブ型は VM上で数値として扱われる」という点です。つまり、

プリミティブ型の値は「ポインタのような非ポインタ」である可能性があります。したがって、GC

を正確な GCとするためには、プリミティブ型と参照型の変数を識別しなければなりません。

5.2 HotspotVMの構造

参照型の識別の前に、HotspotVMの構造を簡単に説明しておきましょう。

まず、HotspotVM は基本的にバイトコード(.class ファイル)内の命令セットを 1 つずつ読み

込んで、命令に従った処理をこなします。命令セットは実行する操作を定義した 1バイトの「オペ

コード」と、操作が用いるデータとなる「オペランド」で構成されています。オペコードは実際には

0x32のようなただのバイト列です。しかし、これでは人間が読むにはあまりに辛いため、通常、オ

ペコードを人間が読めるような aaloadという形式で表現します。これを「ニーモニック」と呼び

ます。

そして、HotspotVM には JVM スタックとフレームというものがあります。C のコールスタッ

Page 37: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.3. HOTSPOTVMの実行フロー

ク、コールフレームと役割は同じです。Java上のメソッドが呼ばれると対応するフレームが JVM

スタックに積まれ、メソッドの実行が終了するとフレームが JVMスタックから降ろされます。

図 5.1 JVMスタック、フレーム

また、フレームの中にはローカル変数とオペランドスタックというものがあります。ローカル変

数はメソッド内で使用するローカル変数を格納する部分です。また、メソッドの引数もローカル変

数として扱われます。

HotspotVM は「スタックマシン」です。そのため、VM 上の計算はスタックを使って処理しま

す。HotspotVMはメソッドフレーム内のオペランドスタックを使って計算を行います。

5.3 HotspotVMの実行フロー

では、実際のサンプルコードを使って、ローカル変数とオペランドスタックがどのように使われ

るか見ていきましょう。

リスト 5.2: TwoDifferentLocalVars.java

1: class TwoDifferentLocalVars {

2: public static void main(String args[]){

3: int primitiveType = 1; // プリミティブ型4: Object referenceType = new Object(); // 参照型5: }

31

Page 38: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.3. HOTSPOTVMの実行フロー

6: }

リスト 5.2の main()メソッドはプリミティブ型と参照型をローカル変数に格納するシンプルな

メソッドです。このメソッドを Javaバイトコードに変換したものがリスト 5.3です。

リスト 5.3: TwoDifferentLocalVars.java:バイトコード

pc( 0): iconst_1

pc( 1): istore_1

pc( 2): new #2 // class java/lang/Object

pc( 5): dup

pc( 6): invokespecial #1 // Method java/lang/Object."<init>"

pc( 9): astore_2

pc(10): return

リスト 5.3内のバイトコードに振られている番号は行番号ではなく、メソッド内のバイトコード

に一意に振られているプログラムカウンタ (以下、pc)です。HotspotVMはリスト 5.3を上から順

番に実行し、ローカル変数にプリミティブ型と参照型の値を格納します。バイトコードは一見難し

そうに見えますが、VMの実行フローのイメージと命令セットの意味が多少理解できれば、読み解

くのはそれほど難しくありません。

表 5.1 ニーモニックと命令内容

ニーモニック 命令内容

iconst_’i’ ’i’の部分にあたる intの定数をオペランドスタックに積むistore_’n’ ローカル変数配列の’n’番目にオペランドスタックの先頭の int型の値を格納するnew 新たなオブジェクトを生成し、オペランドスタックに積む

dup オペランドスタックの先頭にある値を複製し、オペランドスタックに複製した値を積む

invokespecial インスタンス初期化メソッド等の特殊なメソッドを呼び出す

astore_’n’ ローカル変数配列の’n’番目にオペランドスタックの先頭の参照型の値を格納するreturn メソッドから voidをリターンする

表 5.1にはリスト 5.3に登場するニーモニックとその命令内容を示しています。

32

Page 39: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.3. HOTSPOTVMの実行フロー

図 5.2 バイトコード実行フロー

33

Page 40: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.4. 参照マップ

図 5.2 はバイトコードの実行フローを示しています。最終的にローカル変数 1 に 1 が格納され、

ローカル変数 2には Objectクラスのインスタンスのアドレスが格納されています。ローカル変数

1はリスト 5.2内の primitiveType変数であり、ローカル変数 2は referenceType変数です。

また、上記の様にバイトコードを読み込みながら命令セットを 1つずつ実行するインタプリタを

「バイトコードインタプリタ」といいます。

さて、GCの話に戻りましょう。もし、プログラムカウンタ 10(以下、pc10)の状態で GCが実

行された場合は、GCはローカル変数 2が参照するオブジェクトのみを「確実に生きている」と判断

しなければなりません。そして、プリミティブ型の値が格納されているローカル変数 1は GC対象

ではないということを識別する必要があります。HotspotVM はどのようにしてローカル変数(ま

たはオペランドスタック)内の値を識別しているのでしょうか?

5.4 参照マップ

ここで注目するのは Javaの型情報です。リスト 5.3を見るとローカル変数やオペランドスタック

に格納される際のニーモニックが、プリミティブ型と参照型で異なっていることがわかります。プ

リミティブ型の場合、istore_1となっており、参照型の場合は astore_2となっていますね。

HotspotVMはバイトコードの型情報を利用して、GC発生時のフレーム内の「参照マップ」とい

うものを作成します。参照マップとはその名前の通り、参照型を格納しているローカル変数やオペ

ランドスタックの位置を示した地図です。実際の参照マップは 00100というようなビット列です。

この参照マップは、ビット列のビットが立っている部分に対応するローカル変数(またはオペラン

ドスタック)に参照型の値が格納されていることを示しています。

5.5 抽象的インタプリタ

参照マップは「抽象的インタプリタ」によって作成されます。「抽象的インタプリタ」とは簡単に

いえば「型情報のみを記録するインタプリタ」のことです。抽象的インタプリタはバイトコードを

実行した結果、ローカル変数とオペランドスタック内に格納された値の型のみを記録します。実際

に格納された値について、抽象的インタプリタは無関心です。

では、前の「HotspotVMの実行フロー」で説明したバイトコード(リスト 5.3)を使って、抽象

的インタプリタと通常のインタプリタとでは具体的にどのように動作が異なるのか見比べていきま

しょう。

リスト 5.4: TwoDifferentLocalVars.java:バイトコード実行フロー(抽象的インタプリタ)

BasicBlock#0

pc( 0): locals = ’r ’, stack = ’’ // iconst_1

pc( 1): locals = ’r ’, stack = ’v’ // istore_1

pc( 2): locals = ’rv ’, stack = ’’ // new #2

pc( 5): locals = ’rv ’, stack = ’r’ // dup

pc( 6): locals = ’rv ’, stack = ’rr’ // invokespecial #1

pc( 9): locals = ’rv ’, stack = ’r’ // astore_2

pc(10): locals = ’rvr’, stack = ’’ // return

34

Page 41: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.6. 参照マップの作成

抽象的インタプリタの実行フローは簡単に書き表すことができます。リスト 5.4 がその実行フ

ローです。リスト 5.4 中の locals はローカル変数、stack はオペランドスタックです。ローカ

ル変数やオペランドスタックにある r(reference)は参照型、v(value)はプリミティブ型と考え

てください。ローカル変数内の半角スペースはまだ初期化されていないという意味です。また、

BasicBlock#0の役割については後の「5.7 条件分岐時の参照マップ」の項で説明しますので、今は

無視してください。

抽象的インタプリタは、ある命令セット実行前のローカル変数とオペランドスタックの型情報を

記録します。例えば pc0 では iconst_1 の実行前の型情報が記録されています。そのため、ロー

カル変数(locals)には引数の args の型を表す r のみが記録されています。続いて、pc1 では

iconst_1の実行した結果、オペランドスタック(stack)には 1の型を表す vのみが記録されてい

ます。

このように、抽象的インタプリタは実際の値は気にせず、型情報のみを淡々と記録します。そし

て、参照マップは抽象的インタプリタが記録したバイトコードに対応する型情報から作成されます。

5.6 参照マップの作成

GCは基本的に命令セット実行中の様々なタイミングで発生します。オブジェクトを生成する命

令セット実行中に GCが発生するかもしれませんし、足し算の行う命令セット実行中に GCが発生

するかもしれません。

もしある命令セット実行中に GC が発生したとき、フレーム内のローカル変数とオペランドス

タック内にある参照型が指すオブジェクトは GCに確実に生きていると判断されなければなりませ

ん。そのためには、GCが発生時の命令セット実行時の参照マップを作成する必要があります。

リスト 5.4の pc5の命令セット(dupオペコード)を実行中に GCが発生したと想定し、どのよ

うに参照マップが作成されるかを見ていきましょう。

35

Page 42: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.7. 条件分岐時の参照マップ

図 5.3 参照マップ作成

図 5.3は生成した参照マップを示しています。ローカル変数 0とオペランドスタックの先頭に対

応するビットが立っていることがわかります。GCは、この参照マップを見て、ローカル変数 0と

オペランドスタックの先頭に参照型の値が格納されていると判断し、それらが参照するオブジェク

トは GCに確実に「生きている」と判断されます。

5.7 条件分岐時の参照マップ

今までは条件分岐なしのサンプルプログラムを例にとって参照マップの作成を見てきましたが、

実は条件分岐が 1つ入るだけで、参照マップの作成は格段と難しくなります。次のサンプルコード

を見てください。

リスト 5.5: TwoControlPath.java

1: class TwoControlPath {

2: static public void main(String args[]){

3: if (args.length == 0) {

4: Object referenceType = new Object();

5: return;

6: } else {

7: int primitiveType = 1;

8: return;

9: }

10: }

11: }

リスト 5.5 は引数である args のサイズを判断して、ローカル変数の referenceType か、

primitiveTypeにそれぞれ値を格納します。

36

Page 43: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.7. 条件分岐時の参照マップ

リスト 5.5の main()メソッドのバイトコードは次の通りです。

リスト 5.6: TwoControlPath.java:バイトコード

pc( 0): aload_0

pc( 1): arraylength

pc( 2): ifne 14

pc( 5): new #2 // class java/lang/Object

pc( 8): dup

pc( 9): invokespecial #1 // Method java/lang/Object."<init>"

pc(12): astore_1

pc(13): return

pc(14): iconst_1

pc(15): istore_1

pc(16): return

リスト 5.6には新しく ifneというニーモニックが登場しています。ifneの命令内容は「オペラ

ンドスタックの先頭の int型の値を取り出し、その値は 0でなければ指定した pcにジャンプする」

です。pc2では ifne 14となっていますので、オペランドスタックの先頭の値(args.length)が

0ではない場合、pc14にジャンプします。

さて、ここで注目して欲しいのは pc12と pc15です。どちらともローカル変数 1に対して、プリ

ミティブ型、参照型の値をそれぞれ格納しています。つまり、条件分岐によってはローカル変数に

格納される値が変わるということです。pc13の時点で GCが発生すれば、ローカル変数 1の型は参

照型ですが、pc16の時点で GCが発生すれば、ローカル変数 1の型はプリミティブ型なのです。

そのため、抽象的インタプリタはすべての状況における型情報を記録しなければなりません。リ

スト 5.5では args.lengthが 0であった場合と、それ以外の場合の型情報を記録しなければなりま

せん。

そこで抽象的インタプリタはバイトコードを「ベーシックブロック」という単位に切り分けます。

リスト 5.6の場合は次のようになります。

リスト 5.7: TwoControlPath.java:バイトコード実行フロー(抽象的インタプリタ)

BasicBlock#0

pc( 0): locals = ’r ’ stack = ’’ // aload_0

pc( 1): locals = ’r ’ stack = ’r’ // arraylength

pc( 2): locals = ’r ’ stack = ’v’ // ifne 14

BasicBlock#2

pc( 5): locals = ’r ’ stack = ’’ // new

pc( 8): locals = ’r ’ stack = ’r’ // dup

pc( 9): locals = ’r ’ stack = ’rr’ // invokespecial

pc(12): locals = ’r ’ stack = ’r’ // astore_1

pc(13): locals = ’rr’ stack = ’’ // return

BasicBlock#1

pc(14): locals = ’r ’ stack = ’’ // iconst_1

pc(15): locals = ’r ’ stack = ’v’ // istore_1

pc(16): locals = ’rv’ stack = ’’ // return

37

Page 44: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.8. メソッド呼び出し時の参照マップ

リスト 5.7 を見ると、if 文の真文、偽文のそれぞれがベーシックブロックに分けられている

ことがわかります(BasicBlock#1、BasicBlock#2)。BasicBlock#1の pc14と、BasicBlock#2

の pc5 のローカル変数、オペランドスタックの型情報は、BasicBlock#0 の pc2 の実行後のも

のです。BasicBlock#2 では真文のバイトコードを実行しその型情報を記録しています。一方、

BasicBlock#1は偽文の型情報を記録しています。

また、リスト 5.7の pc13と pc16の時点のローカル変数 1の型情報が異なる点に注目してくださ

い。つまり、pc13と pc16の時点で GCが発生してもローカル変数 1の型を参照マップによってき

ちんと識別することができます。

このベーシックブロックという仕組みによって、メソッド内の様々な状況の型情報を記録するこ

とができます。ベーシックブロックは条件分岐だけではなく、ループや switch文、try-catch文等

にも同様に使用されます。リスト 5.2のように条件分岐等がメソッド内に登場に登場しない場合は、

メソッド内のすべてのバイトコードが BasicBlock#0として扱われます。

5.8 メソッド呼び出し時の参照マップ

今までは実行中フレームの参照マップがどのように作られるかを見てきました。しかし、JVMス

タックにフレームが 1個以上積まれていた場合、実行中フレームの下にあるフレームはメソッド呼

び出し時の参照マップを作ることになります。

図 5.4 各フレームの参照マップ作成

次はメソッド呼び出し時の参照マップがどのように作られるかを見てみましょう。次のサンプル

コードを見てください。

リスト 5.8: MethodCall.java

38

Page 45: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.8. メソッド呼び出し時の参照マップ

1: class MethodCall {

2: static public void main(String args[]){

3: Object referenceType = new Object();

4: int primitiveType = 1;

5: something(referenceType, primitiveType);

6: }

7:

8: static void gcCall(Object a, int b){

9: System.gc(); // GCの実行10: }

11: }

リスト 5.8 がリスト 5.2 と違う点は 5 行目の gcStart() メソッドを呼び出している点です。

gcStart()メソッドは GCの実行を行うメソッドです。参照型とプリミティブ型の引数を受け取り

ますが、これは説明のためのもので、実際には使用しません。

リスト 5.8の main()メソッドのバイトコードは次のようになります。

リスト 5.9: MethodCall.java:バイトコード

pc( 0): iconst_1

pc( 1): istore_1

pc( 2): new #2 // class java/lang/Object

pc( 5): dup

pc( 6): invokespecial #1 // Method java/lang/Object."<init>"

pc( 9): astore_2

pc(10): iload_1

pc(11): aload_2

pc(12): invokestatic #3 // Method gcStart

pc(15): return

リスト 5.9 がリスト 5.3 と異なるのは pc10~pc15 です。ここでは gcStart() メソッドの呼び

出しを行っています。pc10、pc11 でオペランドスタックに gcStart() メソッド用の引数を積み、

pc12で gcStart()メソッドを呼び出します。

次に、リスト 5.9を抽象的インタプリタにかけてみましょう。

リスト 5.10: TwoControlPath.java:バイトコード実行フロー(抽象的インタプリタ)

BasicBlock#0

pc( 0): locals = ’r ’ stack = ’’ // iconst_1

pc( 1): locals = ’r ’ stack = ’v’ // istore_1

pc( 2): locals = ’rv ’ stack = ’’ // new

pc( 5): locals = ’rv ’ stack = ’r’ // dup

pc( 6): locals = ’rv ’ stack = ’rr’ // invokespecial

pc( 9): locals = ’rv ’ stack = ’r’ // astore_2

pc(10): locals = ’rvr’ stack = ’’ // iload_1

pc(11): locals = ’rvr’ stack = ’v’ // aload_2

pc(12): locals = ’rvr’ stack = ’vr’ // invokestatic

pc(15): locals = ’rvr’ stack = ’’ // return

39

Page 46: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.9. ハンドルエリアとハンドルマーク

gcCall()メソッドで GCが発生しますので、参照マップはリスト 5.10の pc12時点のものが作

成されます。

図 5.5 参照マップ作成(メソッド呼び出し時)

図 5.5を見てください。実はメソッド呼び出し時の参照マップを作成する際、メソッドの引数に

渡すオペランドスタックの値を無視します。これは引数として渡すオペランドスタックの値が呼び

出したメソッドのローカル変数として扱われるためです。無視したオペランドスタックの値は、呼

び出したメソッドのローカル変数として参照マップを使って正しく識別されます。

5.9 ハンドルエリアとハンドルマーク

これまでは JVM スタックに対する正確な GC のための工夫を見てきました。ここからはネイ

ティブな (C++の)コールスタックに対する工夫を見ていきましょう。

HotspotVMは「ハンドルエリア」と「ハンドルマーク」を使ってコールスタック内のオブジェク

トへのポインタを管理しています。V8は HotspotVMを参考にして作られていますので、正確には

V8が HotspotVMのやり方を真似したということになります。

リスト 5.11はハンドラのみを生成するサンプルコードです。

リスト 5.11: ハンドラの生成

1: void make_handles(oop obj1, oop obj2) {

2: Handle h1(obj1); // ハンドラ 1生成3: Handle h2(obj2); // ハンドラ 2生成4: }

40

Page 47: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

第 5章 正確な GCへの道(HotspotVM編) 5.9. ハンドルエリアとハンドルマーク

HotspotVMのハンドラはスレッド毎にある「ハンドルエリア」に確保されます。そのため、リス

ト 5.11で生成されたハンドラは図 5.6の様に確保されます。

図 5.6 make_handles()実行イメージ

このままだと、ハンドラが確保されたままになってしまうので、HotspotVMにはもう 1つ「ハン

ドルマーク」というハンドルスコープとほぼ同じ機能があります。

リスト 5.12はリスト 5.11にハンドルマークを追加したサンプルコードです。

リスト 5.12: ハンドラの生成:ハンドルマーク有り

1: void make_handles(oop obj1, oop obj2) {

2: HandleMark hm;

3: Handle h1(obj1);

4: Handle h2(obj2);

5: }

2行目に登場する HandleMarkクラスは、コンストラクタでハンドルアリーナの先頭をマーク(記

録)します。そして、HandleMarkクラスのデストラクタでは、マークしておいた位置にハンドルア

リーナの先頭を移動させます。

図 5.7はリスト 5.12の実行イメージです。

図 5.7 make_handles()実行イメージ:ハンドルマーク有り

41

Page 48: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

42

付録 A

参考文献

[1] 中村成洋・相川光著/竹内郁雄監修 『ガベージコレクションのアルゴリズムと実装』 秀和シ

ステム、2010

[2] 中村成洋著 『徹底解剖「G1GC」 アルゴリズム編』 達人出版会、2011

Page 49: 徹底解剖「G1GC - narihiro.infog1gc ヒープの方はリージョン単位で「確保」されます。 図2.3 (3) オブジェクトのアロケーション ここからはパーマネント領域のアロケーションについては説明から除外し、g1gc

徹底解剖「G1GC」実装編(執筆中)

著 者 中村成洋

発行所 www.narihiro.info 

(C) 2011 Narihiro Nakamura