
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から渡されたデータを分析・収集する
Aggregator と Aggregation の 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 アノテーションで、G1GC や GENERATIONAL や ZGC など、どのイベントソースを扱うかを定義します。
register() でイベントタイプを指定して、イベントのコンシューマを登録します。
イベント受信時に、aggregation() により、データ分析・収集クラスである Aggregation が取得できるので、分析を依頼します。
Aggregation では、Aggregator から渡されたデータを分析・収集します。
@Collates(FooAggregator.class) public class FooAggregation extends Aggregation { public void add(...) { } }
@Collates では、Aggregator となるクラスを指定します。解析実行時に、この指定により、Aggregator の実装クラスがインスタンス化されて利用されます。
Aggregator と Aggregation が循環参照する形となり、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
多くの場合は GenerationalGCEvent と G1GCEvent を扱うことになるでしょう。
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ライセンスで利用できるのは嬉しい限りです。
簡単なビューワーを作成したので、良ければどうぞ。