Kirk Pepperdine ships GC Log Analysis tool

Kirk Pepperdine shipped v0.1.0 of gcsee-jma: a Quarkus web app that ingests a JVM garbage-collection log and renders an interactive dashboard of summary, analytics, and per-feature charts. It supports G1, Parallel, Serial, CMS, and generational ZGC, and can be run directly as a jar or in Docker, which is an important feature if your main JVM is not Java 25.

This is a bit of a big deal: Kirk has been doing this, specifically this, for roughly the entire history of "this" being a thing anyone in the Java ecosystem could do. He's a Java Champion, and co-founded jClarity, which Microsoft bought to get its performance-tuning chops in-house. He wrote and taught the Java performance-tuning course that several generations of JVM operators learned from, the one where you walk in thinking you know what a stop-the-world pause is and walk out understanding why your p99 is what it is. He's been parsing GC logs, by eye and by tool, since before the unified logging framework existed, back when each collector emitted its own quirky format and you had to know which version of HotSpot wrote which line. If anyone has earned the right to ship a GC analyzer in 2026 and have people pay attention, it's Kirk.

That matters because the space is littered with GC tools. GCViewer is still around. GCEasy is a commercial service that does the upload-and-analyze thing pretty well. The various APM vendors all have GC dashboards, more or less. Most of them either stopped tracking collector evolution somewhere around Java 11, or they only really work on hosted infrastructure, or they want your logs sent to someone else's servers, or all three. A local-first, modern-collector-aware, "drop a file in and look at it" tool from someone who has been defining what useful GC analysis looks like for two decades is a different proposition than yet another dashboard.

What it actually is

gcsee-jma is a Quarkus web app, packaged as an uber-jar or a multi-arch container image. You start it, you open http://localhost:8080, you upload a GC log, you click "Analyze", and you get a dashboard. The parsing engine underneath is GCSee, which Kirk has been refining for years and which makes this work across the modern collector set without lying to you about what the log actually says.

JMA runs on Java 25, and only on Java 25, but the logs it reads can come from much older JVMs. Practically, you can easily feed JMA logs from anywhere in the Java 11 through Java 25 range. The analyzer is running on a recent LTS; the logs it analyzes don't have to come from the same VM.

Supported collectors, from the README: G1, Parallel, Serial, CMS, and generational ZGC. ZGC's generational mode is the default ZGC in Java 25 (the non-generational variant was removed), and most existing GC tooling has not caught up to its log format. JMA does.

It's AGPL-3.0, which is the polite way of saying "use it freely, host it for your team, but if you build a SaaS around it, you owe the world your modifications." That's a fair posture for a tool that exists to keep people from depending on closed services.

Running it

There are two paths for execution. The first is the jar itself, assuming you're running Java 25; after downloading from the GitHub releases page, run:

java -jar gcsee-jma-0.1.0.jar

To bind to localhost only (the default is all interfaces, which you almost certainly don't want on a shared box):

java -Dquarkus.http.host=127.0.0.1 -jar gcsee-jma-0.1.0.jar

The second is the container:

docker run --rm -p 8080:8080 ghcr.io/kcpeppe/gcsee-jma:latest

The image is multi-arch (amd64 and arm64), so the same tag works on a Linux server and a Mac laptop. No JDK install required locally; the image carries a JRE.

After startup, wait a few seconds, open http://localhost:8080, and select your GC log. It's a minimalistic user interface: drag in a log, and get a populated analysis.

The log analysis is quite in-depth and gives a practitioner everything they'd want to see for GC analysis, including recommendations, concerns, and graphs.

That's the whole story for getting it running. The fundamental thing, though, the part the README doesn't cover for people who aren't already aware, is getting it a log it can read.

Capturing a GC log

The unified logging framework (-Xlog) has been the way to ask the JVM for GC logs since Java 9, and the form has been stable enough since Java 11 that one recipe will serve you from Java 11 all the way through Java 25. The catch is that -Xlog is precise: what you pass determines what you get, and a parser like GCSee needs specific decorators to do its job. Get the flag right once, save it in your launch script, and you can stop thinking about it.

The minimum useful recipe, which is what you want for almost any production capture:

-Xlog:gc*:file=gc.log:utctime,uptime,level,tags:filecount=10,filesize=50M

gc* says "all GC tags." That's the events themselves plus the phases, ages, references, and ergonomics output that an analysis depends on. If you write just gc, you'll get top-level events and JMA will run, but the per-phase charts will be missing data because the phase events live behind gc+phases. Use the asterisk.

file=gc.log writes to a file rather than stdout. If you don't include this, the GC log gets interleaved with your application's stdout, which is a misery for parsing and an even bigger misery for redirecting around log rotation. Always write to a file.

utctime,uptime,level,tags is the decorator set. utctime gives you wall-clock timestamps in UTC, which is what you want when you're correlating with anything else (metrics, traces, paging logs). uptime gives you seconds since JVM start, which is what GCSee uses to compute throughput and pause distributions. level and tags are how the parser tells events apart in the unified-logging stream. Drop any of these and the parser either loses fidelity or refuses the file. Keep all four.

filecount=10,filesize=50M is rotation: ten files, fifty megabytes each, half a gigabyte of rolling history. That's enough for most services to retain a few hours of GC activity, and the rotation means a runaway service doesn't fill your disk. If you're capturing for an investigation rather than running continuously, drop the rotation and let it grow.

There are knobs you can add to the recipe when you need more, and they're worth knowing about even if you don't reach for them daily.

For pause-time forensics on G1, where you're chasing why a specific collection took longer than you expected, add gc+phases=debug for full per-phase timings and gc+heap=debug for region-level accounting:

-Xlog:gc*,gc+phases=debug,gc+heap=debug:file=gc.log:utctime,uptime,level,tags:filecount=10,filesize=50M

Those produce a lot more data, which is exactly what you want when you're trying to figure out what happened, and exactly what you don't want when you're running steady-state. Turn them on for the investigation, turn them off after.

For object-age and tenuring analysis, which matters a lot more on Parallel and on classic G1 than on ZGC, add gc+age*=trace:

-Xlog:gc*,gc+age*=trace:file=gc.log:utctime,uptime,level,tags:filecount=10,filesize=50M

This is the data JMA's per-feature charts use to show you where the survivor space pressure is coming from. On a service that's tenuring more than it should, this is the difference between "GC is acting weird" and "objects of this age class are being promoted out of young because the survivor target is full, and here's why."

ZGC, where the version actually matters

ZGC is the one collector where the JVM version changes the picture, and it's worth a section to itself.

On Java 17, ZGC exists and is production-supported, but it's not generational. You enable it with -XX:+UseZGC, and the standard -Xlog:gc* recipe captures it just fine. JMA will read the log, but the "generational" charts won't have anything to show because the collector simply isn't generational in this version: it's a single-generation collector, and the rest of the dashboard tells you what you need to know.

On Java 21, generational ZGC exists as an opt-in: -XX:+UseZGC -XX:+ZGenerational. If you flip the flag, JMA's generational-ZGC charts light up. If you don't, you're back to the Java 17 picture, and the dashboard reflects that.

On Java 25, the -XX:+ZGenerational flag is gone because the non-generational ZGC has been removed. -XX:+UseZGC gives you generational ZGC. The log format is what JMA was built around in the first place, so this is the cleanest case.

The takeaway, for the impatient: the -Xlog recipe doesn't change across versions, but the collector changes underneath it, and the dashboard reflects what your collector actually emitted.

The thing to watch

If you're capturing from an existing service that's already writing a GC log with different flags, check the decorator string before you ship that log to JMA. A log written with time (local time) instead of utctime, or with pid and tid but no tags, will not parse cleanly. The fix is usually a five-character edit to the launch script and a restart, but if you skip it, you get a dashboard with mysterious gaps and no obvious cause.

Why local-first matters

There's a subtext to all of this that's worth saying: GC logs leak. Not in the dramatic sense, but in the operational sense: they encode heap sizes, allocation patterns, occupancy curves, request shapes by inference, sometimes the cadence of upstream traffic by inference.

They are not secrets, but they're not nothing, and shipping them to a third-party SaaS for analysis is a thing teams in regulated environments quietly avoid even when nobody writes it down. A tool you run on your laptop or on a shared box inside your perimeter doesn't have that problem. You read your own logs in your own isolated room.

That's where the lineage of this tool shows up. Kirk has spent twenty-plus years showing teams how to read GC logs themselves, on the assumption that the people closest to the workload are the ones best positioned to understand what the collector is telling them. A local dashboard with a serious parser underneath is the natural shape of that philosophy. Use it, file issues against it, and write down what you learned from your own logs.

This is a skill worth having, and a tool worth using. Given its pedigree, it will improve your Java development more than you think it will.

Comments (0)

Sign in to comment

No comments yet.