Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

TLABとフィラー(詰め物)

以前、TLABとBump the Pointerについて、検証しました。

www.sakatakoichi.com

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を押し、ヒストグラムを見ます。

f:id:jyukutyo:20190121175347p:plain

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というリンクがあるのです。

f:id:jyukutyo:20190121175613p:plain

Unreachable Objects、つまり到達不能オブジェクトです。GCのルートから、たどり着けないオブジェクトを表し、死んだオブジェクトですので、GCで回収される対象のオブジェクトです。

f:id:jyukutyo:20190121175317p:plain

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配列をフィラーにして使っているようです。