MS 製 JavaGC ログ解析ライブラリ GCToolKit の使い方


GCToolKit とは

2021年8月にオープンソース化された MS製の Javaガベージコレクション(GC)ログファイルのパーサライブラリです。

GCToolKit は、ログファイルを読み込み、正規表現で解析し、GC/セーフポイントのイベントを発行するので、このイベントを集計して解析を行うことができます。

デフォルトでは Vert.x がバックエンドとして使われ、リアクティブ・ストリーム上でパースとイベントへの変換が行われるので、このイベントをサブスクライブする形となります。


GCToolKit の導入

以下の3つのモジュールが提供されています。

  • gctoolkit-api : ログ解析のエントリーポイントとなるAPIを提供
  • gctoolkit-parser : ログパーサー(gctoolkit-api に依存)
  • gctoolkit-vertx : Vert.xがバックエンド(gctoolkit-api に依存)

DataSourceChannel JVMEventChannel の実装を自身で用意することで Vert.x を用いないこともできますが、通常は以下の依存を定義となるでしょう。

dependencies {
    implementation("com.microsoft.gctoolkit:gctoolkit-parser:3.7.0")
    implementation("com.microsoft.gctoolkit:gctoolkit-vertx:3.7.0")
}

ログ解析は以下のような流れになります。

var logFile = new SingleGCLogFile(logPath);
var gcToolKit = new GCToolKit();
JavaVirtualMachine jvm = gcToolKit.analyze(logFile);
Optional<FooAggregation> results = jvm.getAggregation(FooAggregation.class);
  • DataSource インターフェースの実装クラス SingleGCLogFile を生成
    • RotatingGCLogFile でディレクトリを指定することもできる
  • GCToolKit を生成し、analyze()DataSource を渡す
  • JavaVirtualMachine から解析結果を取得


Aggregator と Aggregation

GCToolKit では、Aggregator と Aggregation の実装を用意することでログ解析を行います。

  • Aggregator : イベントをキャプチャして Aggregation にイベント値の分析を依頼する
  • Aggregation : Aggregator から渡されたデータを分析・収集する

AggregatorAggregation の API 設計は無駄に分かりにくいので、概略を例示します。

Aggregator の実装は以下のようになります。

@Aggregates({ EventSource.G1GC })
public class FooAggregator extends Aggregator<FooAggregation> {

    public FooAggregator(FooAggregation results) {
        super(results);
        register(G1GCPauseEvent.class, (G1GCPauseEvent e) -> aggregation().add(...));
    }
}

Aggregator では、@Aggregates アノテーションで、G1GCGENERATIONALZGC など、どのイベントソースを扱うかを定義します。

register() でイベントタイプを指定して、イベントのコンシューマを登録します。

イベント受信時に、aggregation() により、データ分析・収集クラスである Aggregation が取得できるので、分析を依頼します。


Aggregation では、Aggregator から渡されたデータを分析・収集します。

@Collates(FooAggregator.class)
public class FooAggregation extends Aggregation {
    public void add(...) {  }
}

@Collates では、Aggregator となるクラスを指定します。解析実行時に、この指定により、Aggregator の実装クラスがインスタンス化されて利用されます。

AggregatorAggregation が循環参照する形となり、API設計として終わってる感が否めないですが、いたしかたありません。


FooAggregation は ServiceLoader 登録しておけば自動でピックされますが、以下のように明示的にロードする方がシンプルでしょう。

var gcToolKit = new GCToolKit();
gcToolKit.loadAggregation(new FooAggregation());


JVMEvent

Aggregator で受信するイベントの改装は以下のようになっています(全量は記載していません)。

JVMEvent
 ├ ApplicationRunTime
 ├ ApplicationConcurrentTime
 ├ ApplicationStoppedTime
 ├ SurvivorRecord
 ├ Safepoint
 ├ JVMTermination
 └ GCEvent
    ├ GenerationalGCEvent
    │   ├ GenerationalGCPauseEvent
    │   ├ ...
    │   └ CMSConcurrentEvent
    ├ G1GCEvent
    │   ├ G1GCPauseEvent
    │   │   ├ G1Trap
    │   │   └ G1RealPause
    │   └ G1GCConcurrentEvent
    │        ├ ConcurrentScanRootRegion
    │        ├ G1ConcurrentRebuildRememberedSets
    │        ├ G1ConcurrentMarkResetForOverflow
    │        ├ ConcurrentCreateLiveData
    │        ├ ConcurrentCompleteCleanup
    │        ├ ConcurrentCleanupForNextMark
    │        ├ G1ConcurrentUndoCycle
    │        ├ G1ConcurrentMark
    │        ├ G1ConcurrentCleanup
    │        ├ G1ConcurrentStringDeduplication
    │        └ ConcurrentClearClaimedMarks
    ├ FullZGCCycle (3.7 以降で ZGCFullCollection に変更)
    ├ MajorZGCCycle (3.7 以降で ZGCYoungCollection に変更)
    ├ MinorZGCCycle (3.7 以降で ZGCOldCollection に変更)
    └ ShenandoahCycle

多くの場合は GenerationalGCEventG1GCEvent を扱うことになるでしょう。


GCToolKit の利用

GC 後のヒープ使用量を得る例は以下のようになります。

Aggregator

@Aggregates({ EventSource.G1GC, EventSource.GENERATIONAL })
public class HeapOccupancyAggregator extends Aggregator<HeapOccupancyAggregation> {

    public HeapOccupancyAggregator(HeapOccupancyAggregation results) {
        super(results);
        register(GenerationalGCPauseEvent.class, this::extractHeapOccupancy);
        register(G1GCPauseEvent.class, this::extractHeapOccupancy);
    }

    private void extractHeapOccupancy(GenerationalGCPauseEvent event) {
        if (event.getHeap() == null) return;
        aggregation().addDataPoint(
                event.getGarbageCollectionType(),
                event.getDateTimeStamp(),
                event.getHeap().getOccupancyAfterCollection());
    }

    private void extractHeapOccupancy(G1GCPauseEvent event) {
        if (event.getHeap() == null) return;
        aggregation().addDataPoint(
                event.getGarbageCollectionType(),
                event.getDateTimeStamp(),
                event.getHeap().getOccupancyAfterCollection());
    }

}

Aggregation

@Collates(HeapOccupancyAggregator.class)
public class HeapOccupancyAggregation extends Aggregation {

    private final Map<GarbageCollectionTypes, DataSet> aggregations = new ConcurrentHashMap<>();

    public void addDataPoint(GarbageCollectionTypes gcType, DateTimeStamp timeStamp, long heapOccupancy) {
        aggregations.computeIfAbsent(gcType,key -> new DataSet())
                .add(timeStamp.toSeconds(), heapOccupancy);
    }

    @Override public boolean hasWarning() { return false; }
    @Override public boolean isEmpty() { return aggregations.isEmpty(); }
    public Map<GarbageCollectionTypes, DataSet> get() { return aggregations; }

    public static class DataSet {

        private final List<Point> dataSeries = new ArrayList<>();
        public void add(Number x, Number y) { dataSeries.add(new Point(x, y)); }
        public Stream<Point> stream() { return dataSeries.stream(); }

        public record Point(Number x, Number y) {
            @Override
            public String toString() {
                return x + "," + y;
            }
        }
    }

}

main

public static void main(String[] args) throws Exception {

    var path = Path.of("../gc.log");
    var logFile = new SingleGCLogFile(path);

    var gcToolKit = new GCToolKit();
    gcToolKit.loadAggregation(new HeapOccupancyAfterCollectionAggregation());

    JavaVirtualMachine jvm = gcToolKit.analyze(logFile);
    Optional<HeapOccupancyAfterCollectionAggregation> results =
        jvm.getAggregation(HeapOccupancyAfterCollectionAggregation.class);

    for (var e : results.get().get().entrySet()) {
        // ...
    }
}


まとめ

GCToolKit の使い方について簡単に見てきました。

GCログは、オプション指定により出力のされ方が様々なので、堅牢なパース処理を書くのはかなり手間なので、gctoolkit-parser が MITライセンスで利用できるのは嬉しい限りです。

簡単なビューワーを作成したので、良ければどうぞ。

github.com