Proving an upgrade was worth it

Java 25 Course · lesson 16 of 16 · 6 min read

Compact headers, AOT and a newer GC are claims - measure them on your workload.

Open this lesson in the learning hub

Key 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.