以前、TLABとBump the Pointerについて、検証しました。
AlekseyさんのJVM Anatomy Parkには、もう1つTLABの話題があります。今回、その内容も、実際に動かして見てみます。
JVM Anatomy Park #5: TLABs and Heap Parsability
こんなコードが掲載されています。
import java.util.*; import java.util.concurrent.*; public class Fillers { public static void main(String... args) throws Exception { final int TRAKTORISTOV = 300; CountDownLatch cdl = new CountDownLatch(TRAKTORISTOV); for (int t = 0 ; t < TRAKTORISTOV; t++) { new Thread(() -> allocateAndWait(cdl)).start(); } cdl.await(); List<Object> l = new ArrayList<>(); new Thread(() -> allocateAndDie(l)).start(); } public static void allocateAndWait(CountDownLatch cdl) { Object o = new Object(); // Request a TLAB cdl.countDown(); while (true) { try { Thread.sleep(1000); } catch (Exception e) { break; } } System.out.println(o); // Use the object } public static void allocateAndDie(Collection<Object> c) { while (true) { c.add(new Object()); } } }
300スレッドでオブジェクトを作り、ラッチで全スレッドが終わるのを待って、リストに無限にオブジェクトを追加してOutOfMemoryErrorを発生させるコードです。各スレッドのTLABに、わずかなオブジェクトを割り当てただけの状態で、OOMEとなるわけです。
これを、Epsilon GCで実行します。そのため、Java 11が必要です。
$ java -Xmx500M -Xms500M -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+HeapDumpOnOutOfMemoryError Fillers java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid13747.hprof ... Heap dump file created [1922110768 bytes in 70.514 secs] Terminating due to java.lang.OutOfMemoryError: Java heap space
Epsilonを使うのは、GCを実行しないので、OOMEをすぐに発生させられるからです。HeapDumpOnOutOfMemoryError
を設定しているので、OOME時にヒープダンプが生成されました。java_pid13747.hprof
という感じのファイルです。
ヒープダンプは、Eclipse Memory Analyzer(通称mat)で見ることにします。matについては、以前所属した会社のブログに書いています。
ヒープダンプをEclipse Memory Analyzerで解析しよう! | [フリュー公式] Tech Blog
matでダンプファイルを開き、解析を待ちます。解析が終わったら、Histogramを押し、ヒストグラムを見ます。
Class Name | Objects | Shallow Heap | Retained Heap ----------------------------------------------------------------------------------------- java.lang.Object | 16,725,594 | 267,609,504 | java.lang.Object[] | 665 | 83,111,568 | byte[] | 4,448 | 233,312 | java.util.HashMap$Node | 4,355 | 139,360 | java.lang.String | 4,350 | 104,400 | java.util.HashMap$Node[] | 649 | 68,352 | java.util.concurrent.ConcurrentHashMap$Node | 1,175 | 37,600 | java.lang.Thread | 305 | 36,600 | char[] | 11 | 33,696 | java.util.HashMap | 664 | 31,872 | java.util.concurrent.ConcurrentHashMap$Node[]| 24 | 14,976 | java.lang.Class | 902 | 12,656 | java.security.AccessControlContext | 308 | 12,320 | java.lang.module.ModuleDescriptor$Exports | 389 | 9,336 | java.util.HashSet | 542 | 8,672 | java.lang.invoke.MemberName | 200 | 8,000 | java.lang.invoke.MethodType | 196 | 7,840 | java.util.Collections$UnmodifiableSet | 464 | 7,424 | int[] | 197 | 7,400 | -----------------------------------------------------------------------------------------
Shallow Heapの降順にしてみると、ObjectとObject配列が多く、とくに不思議なところはありません。実は、もう1つの方を見る必要があります。Overviewに、Unreachable Objects Histogramというリンクがあるのです。
Unreachable Objects、つまり到達不能オブジェクトです。GCのルートから、たどり着けないオブジェクトを表し、死んだオブジェクトですので、GCで回収される対象のオブジェクトです。
Shallow Heapの降順にしてみると、int配列の数と使用ヒープ量が多いことに気づきます。
Class Name | Objects | Shallow Heap ------------------------------------------------- java.lang.Object[] | 557 | 166,166,832 byte[] | 16,309 | 2,091,032 int[] | 1,991 | 1,610,248 java.util.HashMap$Node | 14,329 | 458,528 java.util.HashMap$Node[]| 2,471 | 291,568 -------------------------------------------------
このint配列が、Alekseyさんの記事にあるフィラーです。このフィラーは何かと言うと、TLABで使用していない(オブジェクトを割り当てていない)領域を、埋めているものです。
GCを考えてみましょう。コレクタから見ると、TLAB内の状況というのは把握できないものです。つまり、TLABのどこまでオブジェクトを割り当てていて、どこから未使用であるかは、GCからはわかりません。パーサビリティを持つ意味で、未使用の部分をint配列で埋めているのです。結果として、ヒープダンプを解析すると、その未使用部分は、到達不能オブジェクトで埋まっていることとなります。
では、なぜint配列なのでしょうか?埋める領域は可変なので、サイズを変えられるオブジェクトの方がよいからです。さらに、配列であれば、ヘッダーにレングスを持っているので、パースする際にそのレングスの分を読み飛ばせます。
こうして、TLABはint配列をフィラーにして使っているようです。