Proving an upgrade was worth it
Compact headers, AOT and a newer GC are claims - measure them on your workload.
Open this lesson in the learning hubKey points
- Every release advertises improvements. Whether any of them help your service depends entirely on its shape, so the only honest answer comes from measuring before and after.
- Compact object headers shrink each object header from 12 or 16 bytes to 8. That is a large win for a heap full of small objects and near-irrelevant for one holding big arrays.
- AOT caching cuts startup by recording the loaded class state from a training run. It helps short-lived and scale-to-zero workloads far more than a service that runs for weeks.
- Measure the right things: startup time, steady-state p99, allocation rate, live set after a full GC, and RSS. A single throughput number hides all of the interesting movement.
- Change one thing at a time. Upgrading the JDK and switching collector together makes the result uninterpretable, and you will not know which to keep.
- Keep the old JDK deployable. An upgrade you cannot roll back is a bet rather than a change, and the fastest way to diagnose a regression is to run both side by side.
Example
# Compact object headers - 8 bytes instead of 12/16.
-XX:+UseCompactObjectHeaders
# Typical: 5-15% less heap on object-heavy workloads.
# Near zero on a heap dominated by large arrays or byte buffers.
# AOT cache - two steps: train, then use.
$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar app.jar
$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
-XX:AOTCache=app.aot -jar app.jar
$ java -XX:AOTCache=app.aot -jar app.jar # production
---
# MEASURE THESE, before and after, on the SAME hardware and load:
#
# startup to first request time to a 200 on /actuator/health
# steady-state p99 after warm-up, not during
# allocation rate MB/s from GC logs
# live set heap used AFTER a full GC
# RSS the container limit is on this, not heap
# GC pause p99 and max max is what users felt
# Always log GC - it costs almost nothing and is the only real evidence:
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20M
---
# A DEFENSIBLE UPGRADE SEQUENCE:
#
# 1. baseline on the current JDK, under production-like load
# 2. new JDK, IDENTICAL flags <- measure
# 3. add compact object headers <- measure
# 4. change the collector, if warranted <- measure
# 5. add AOT, if startup matters <- measure
#
# Steps 2-5 done together give one number and no understanding.
# And keep the rollback real:
# - the previous image stays tagged and deployable
# - class files target the OLD release until the new one has soaked
# (--release 21 on a JDK 25 build still runs on 21)
Change one thing, measure startup, p99, live set and RSS - and keep the previous JDK deployable until the new one has soaked.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Java 25 Course course, and every lesson in it is listed on the Java 25 Course contents page.