原文はこちら。
The original article was written by Thomas Schatzl (OpenJDK developer, Oracle).
https://tschatzl.github.io/2022/09/26/jdk-vm-internal-fillerarray.html
JDK 19 のアップデートに関するブログ記事の執筆時に、ヒープダンプに現れる可能性のある「新しい」(jdk.vm.internal.FillerObject および jdk.vm.internal.FillerArray)オブジェクトについて書くべきか検討していました。
JDK 19 G1/Parallel/Serial GC changes
https://tschatzl.github.io/2022/09/16/jdk19-g1-parallel-gc-changes.html
https://logico-jp.io/2022/09/23/jdk-19-g1-parallel-serial-gc-changes/
これらはそれほど興味を引くものではないと考えていましたが、そうではなかったようで、すぐに気づいた人がいたようです。というわけで、この記事で上記オブジェクトの目的について説明します。
The mysterious Ljava.internal.vm.FillerArray;
https://old.reddit.com/r/java/comments/xlnw9q/the_mysterious_ljavainternalvmfillerarray/
Background
Hotspotでは、パース可能性(parsability)と呼ばれる非常に興味深い性質がJavaヒープにはあります。これは、ヒープの一部分(コレクタによって、リージョン、スペース、世代と呼ばれます)を、それぞれの下から上のアドレスまでパース(線形に辿る)できることを意味します。ガベージコレクタは、Javaヒープ内のあるオブジェクトの後には、必ず何らかの有効なJavaオブジェクトが続くことを保証しています。
すべてのオブジェクトヘッダーには、オブジェクトの型(すなわちクラス)に関する情報が含まれているので、そのオブジェクトのサイズを推論できます。
この特性は、様々な方法で利用されています。以下はその一例です。
ヒープ統計 ( jmap -histo ) | ヒープのすべての部分の底から上まで辿り、統計を取る |
ヒープダンプ | 文字通りヒープダンプ |
young世代でヒープを部分的に収集するときに、ヒープの少なくとも一部についてヒープ解析性を要求するコレクターがある | 一部のガベージコレクタでは、関心の対象の参照が存在するおおよその位置を記録するために、不正確な記憶集合 (remembered set) を使用するが、これらの記録領域(記憶集合エントリ)は、ガベージコレクションの際に素早く辿る必要がある。 この仕組みについて、以下の記事の序章で少々詳しく説明している。 Card Table Card Size Shenanigans https://tschatzl.github.io/2022/02/15/card-table-card-size.html https://logico-jp.io/2022/03/16/card-table-card-size-shenanigans/ |
では、どうしてJavaヒープが解析不能になるのでしょうか?結局のところ、オブジェクトは線形に割り当てられているのでしょうか?
原因は2つあります(もっとあるかもしれませんが)。
- 第一は、LAB (Local allocation buffer) の割り当てです。
スレッドはすべてのメモリ割り当てのために他スレッドと同期することはありませんが、他スレッドと同期せずにローカル割り当てに使用する、大きなバッファ (TLAB : Thread local allocation buffer) を切り分けます。これらの LAB は固定サイズです。スレッドが現在のLABにメモリ割り当てができなくなった場合、新しいLABを取得する前に、残りの領域を適切にフォーマットする必要があります。 - 第二には、クラスのアンロードです。
クラスのアンロードにより、そのクラス情報が破棄されるため、クラスがアンロードされたオブジェクトはパースできなくなります。ガベージコレクターによっては、クラスアンロード後にヒープをコンパクトにして、解析可能なJavaヒープ領域からこれらの無効なオブジェクトをすべて削除することで、この問題を回避しているものもあります。
JDK 19までは、Hotspot VMはjava.lang.Object
と integerの配列 ([I
) のインスタンスを使って、こうしたヒープの穴を埋めて(再フォーマットして)いました。java.lang.Object
は極小の穴を埋めるのにのみ使われており、それ以外の場合はすべてintegerの配列を使っています。
つまり、非生存データを含む完全なヒープヒストグラムやヒープダンプに、プログラムのどこからも参照されないjava.lang.Object
や[I
インスタンスが大量にあることに気付いた方もいらっしゃるかと思います。
以下は、古いJVMでの、あるプログラムに対するjmap -histo <pid>
の実行例です。
num #instances #bytes class name (module)
-------------------------------------------------------
1: 16015 350913960 [B (java.base@11.0.16)
2: 467918 33690096 java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask (java.base@11.0.16)
3: 4559 18664488 [I (java.base@11.0.16)
4: 1 2396432 [Ljava.util.concurrent.RunnableScheduledFuture; (java.base@11.0.16)
5: 10715 342880 java.util.concurrent.SynchronousQueue$TransferStack$SNode (java.base@11.0.16)
6: 10528 336896 java.util.concurrent.locks.AbstractQueuedSynchronizer$Node (java.base@11.0.16)
7: 10149 243576 java.lang.String (java.base@11.0.16)
[...]
A large part of the [I
インスタンスの大部分がfillerオブジェクトであると思われます。
The Change
Java ヒープヒストグラムにjdk.vm.internal.FillerObject
およびLjdk.vm.internal.FillerArray;
という名前の特別で明示的なフィラーオブジェクトをJDK-8284435で追加しました(実は、最初の変更ではフィラー配列型の名前に誤記があり、誤ってLjava.vm.internal.FillerArray;
としていましたが JDK-8294000 では修正されています)。
[JDK-8284435] Add dedicated filler objects for known dead Java heap areas
https://bugs.openjdk.org/browse/JDK-8284435
[JDK-8294000] Filler array klass should be in jdk/vm/internal, not in java/vm/internal
https://bugs.openjdk.org/browse/JDK-8294000
これらのクラスは、ほとんどのVM内部クラスと同様に、VM起動時に前もって作成されます。
これらのクラスはフィラー目的の java.lang.Object
や [I
インスタンスと同じ役割を果たしますが、Hotspot VM 開発者にとって、クラッシュログで参照されていたり、この種のオブジェクトへの参照を見かけたら、ガベージオブジェクトへのぶら下がり参照(dangling reference、参照先のない参照)に関連する何らかのバグの可能性が高くなるという利点もあります。これは確実ではありませんが、ぶら下がり参照と以前のフィラーオブジェクトの類いのインスタンスへの正当なリファレンスをより簡単に区別できるようになったため、クラッシュの調査が少し楽になりました。また、ヒープ検証では、ガベージコレクタに依存しないフィラーオブジェクトと生存オブジェクトをよりよく区別することができるようになりました。
以下は、現在のJVMでの、先ほどと同じプログラムに対するjmap -histo
を再実行した例です。
num #instances #bytes class name (module)
-------------------------------------------------------
1: 10740 152377136 [B (java.base@20-internal)
2: 250078 18005616 java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask (java.base@20-internal)
3: 1699 14286176 Ljdk.internal.vm.FillerArray; (java.base@20-internal)
4: 1 1065104 [Ljava.util.concurrent.RunnableScheduledFuture; (java.base@20-internal)
[...]
23: 361 11552 java.lang.module.ModuleDescriptor$Exports (java.base@20-internal)
24: 127 11264 [I (java.base@20-internal)
25: 274 10960 java.lang.invoke.MethodType (java.base@20-internal)
[...]
76: 11 1056 java.lang.reflect.Method (java.base@20-internal)
77: 66 1056 jdk.internal.vm.FillerObject (java.base@20-internal)
78: 1 1048 [Ljava.lang.Integer; (java.base@20-internal)
[...]
このヒストグラムでは、Ljdk.internal.vm.FillerArray;
インスタンスが非常に多く、[I
インスタンスは非常に少ないことに注意してください(これらのヒストグラムはランダムに取得したため、ヒストグラム間の直接比較はできません)。jdk.internal.vm.FillerObject
インスタンスは、最小サイズのホールが少ないために非常に少なくなっています。
(64ビットマシンでは、-XX:-UseCompressedClassPointers
により圧縮クラスポインタを使っていない場合にのみ表示されます。その他の場合は、フィラー配列も最小インスタンスサイズに適合します)
名前とそれに関連するすべての内容は Hotspot VM の内部情報であり、予告なく変更される可能性があります。
Impact Discussion
エンドユーザーにとっては、これらのオブジェクトのインスタンスが完全なヒープダンプに表示されることを除けば、何の変化もないはずです。
今日のところは以上です。謎は解けましたね。