Skip to content

Commit 22b6d75

Browse files
committed
Add blog post about Semeru AOT
1 parent 766b4cd commit 22b6d75

1 file changed

Lines changed: 302 additions & 0 deletions

File tree

_posts/2026-05-04-semeru-scc.adoc

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
---
2+
layout: post
3+
title: 'Faster Startup on IBM Semeru with OpenJ9 Shared Classes Cache'
4+
date: 2026-05-06
5+
tags: performance aot semeru
6+
synopsis: 'Quarkus 3.35 brings great startup improvements to IBM Semeru Runtimes. The same `quarkus.package.jar.aot.enabled=true` flag that activates Project Leyden on OpenJDK now automatically generates an OpenJ9 Shared Classes Cache - no code changes needed.'
7+
author: geoand
8+
---
9+
10+
Slow startup times have long been a challenge for Java applications. Project Leyden addressed this for OpenJDK 25+, but what about IBM Semeru users?
11+
12+
In our https://quarkus.io/blog/leyden-2/[previous post], we described how we integrated Project Leyden into Quarkus, bringing JVM startup way down.
13+
14+
But not everyone runs OpenJDK.
15+
16+
Many teams, especially in enterprise environments, run https://www.ibm.com/semeru-runtimes[IBM Semeru Runtimes], IBM's production Java runtime built on the https://eclipse.dev/openj9/[Eclipse OpenJ9] JVM.
17+
These teams deserve the similar startup improvements, with the same ease of use.
18+
19+
That's what we built. Starting with Quarkus 3.35, the same `quarkus.package.jar.aot.enabled=true` flag that activates Leyden on OpenJDK now automatically generates an OpenJ9 Shared Classes Cache on IBM Semeru.
20+
21+
No code changes. No additional configuration. Quarkus detects the JVM and does the right thing.
22+
23+
== What is the OpenJ9 Shared Classes Cache?
24+
25+
The Eclipse OpenJ9 JVM has long offered a feature called https://eclipse.dev/openj9/docs/shrc/[Shared Classes Cache (SCC)].
26+
The concept is similar to what Project Leyden does for HotSpot, but it predates Leyden by many years and goes further in some respects.
27+
28+
At its core, the SCC is a region of shared memory — either a memory-mapped file (persistent) or shared memory segment (non-persistent) — that stores data the JVM would otherwise have to recompute on every startup.
29+
When a class is loaded for the first time, OpenJ9 automatically stores it in the cache.
30+
On subsequent startups, the JVM finds it there and skips parsing, verification, and loading from disk entirely.
31+
32+
But class data is only the beginning. The SCC stores three categories of data:
33+
34+
Class metadata::
35+
Parsed and verified class structures, ready to use without re-reading JAR files.
36+
37+
AOT-compiled native code::
38+
The OpenJ9 AOT compiler dynamically compiles Java methods into native code at runtime and stores them in the cache. On subsequent runs, these pre-compiled methods execute immediately instead of being interpreted. The VM https://eclipse.dev/openj9/docs/aot/[automatically selects] which methods to AOT-compile using heuristics that identify the startup phase of large applications — no manual configuration needed.
39+
40+
JIT profiling data and compilation hints::
41+
The JIT compiler stores profiling information and optimization hints in the cache, allowing subsequent JVM instances to make better compilation decisions from the start. When a cached AOT method runs, the JIT compiler can further optimize it based on actual runtime behavior, giving you the best of both worlds.
42+
43+
The result: significantly faster startup, lower memory overhead during class loading, and a warmer JIT from the very first request.
44+
45+
Unlike other class data sharing implementations, the OpenJ9 SCC is fully dynamic: it is populated transparently as your application runs, without requiring a separate offline step to enumerate classes.
46+
The cache is also designed for multi-JVM environments — multiple JVM instances can share the same cache simultaneously, reducing the aggregate memory footprint when running several Java applications on the same host.
47+
48+
If you are familiar with how Leyden works, much of this will sound familiar. The key difference is that SCC is specific to the OpenJ9 JVM and has been production-hardened over many years, while Leyden is a newer effort within OpenJDK targeting HotSpot.
49+
50+
== Using it
51+
52+
If you already have `quarkus.package.jar.aot.enabled=true` in your build, you don't need to change anything. Just build with a Semeru JDK:
53+
54+
[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-maven"]
55+
.Maven
56+
----
57+
./mvnw verify -Dquarkus.package.jar.aot.enabled=true -DskipITs=false
58+
----
59+
60+
[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"]
61+
.Gradle
62+
----
63+
./gradlew build quarkusIntTest -Dquarkus.package.jar.aot.enabled=true
64+
----
65+
66+
That's it. The build:
67+
68+
1. Packages your application with the `aot-jar` format.
69+
2. Starts the application with `-Xshareclasses:name=quarkus-app,cacheDir=app-scc`, which tells OpenJ9 to populate the cache as the application runs.
70+
3. Your `@QuarkusIntegrationTest` tests run against it, exercising your endpoints and features. During this time, OpenJ9 is continuously populating the cache with class data, AOT-compiled methods, and JIT profiling hints.
71+
4. When the tests finish and the application shuts down, the `app-scc/` directory contains a fully populated Shared Classes Cache.
72+
73+
Once that is done, verify that your logs contain `Detected IBM Semeru Runtime - using Shared Classes Cache`
74+
75+
To run the application with the cache:
76+
77+
[source, bash]
78+
----
79+
cd target/quarkus-app
80+
java -Xshareclasses:name=quarkus-app,cacheDir=app-scc,readonly -jar quarkus-run.jar
81+
----
82+
83+
The `readonly` flag prevents the cache from being modified at runtime.
84+
This is the recommended setting for production: the cache is an artifact of the build, not something that should drift in production.
85+
86+
The generated `app-scc/` directory must be deployed alongside your JAR:
87+
88+
[source]
89+
----
90+
target/quarkus-app/
91+
├── app-scc/ <1>
92+
├── quarkus-run.jar
93+
├── lib/
94+
└── quarkus/
95+
----
96+
<1> The Shared Classes Cache directory. Its contents are opaque to the user — OpenJ9 manages the internal structure.
97+
98+
=== Customizing the training run
99+
100+
If you need to pass additional JVM flags during the training run (for example, to increase heap size or enable specific JVM features), you can use:
101+
102+
[source, properties]
103+
----
104+
quarkus.package.jar.aot.additional-recording-args=-Xmx512m
105+
----
106+
107+
=== Troubleshooting
108+
109+
If the cache doesn't seem to improve startup, first verify it is actually being used.
110+
Enable verbose output by replacing `readonly` with `readonly,verboseIO`:
111+
112+
[source, bash]
113+
----
114+
java -Xshareclasses:name=quarkus-app,cacheDir=app-scc,readonly,verboseIO -jar quarkus-run.jar
115+
----
116+
117+
This prints detailed information about each class loaded from the cache versus the filesystem.
118+
119+
Also make sure you are using the exact same Semeru JVM version to run the application as was used to build the cache.
120+
OpenJ9 uses internal generation numbers to detect incompatible caches; a version mismatch will cause the cache to be ignored.
121+
122+
== How auto-detection works
123+
124+
When `quarkus.package.jar.aot.enabled=true` is set and no explicit type is configured, Quarkus picks the best strategy for the JVM it detects at build time:
125+
126+
[cols="1,1,1", options="header"]
127+
|===
128+
| Build JVM | Strategy | Output
129+
130+
| IBM Semeru
131+
| Shared Classes Cache (`-Xshareclasses`)
132+
| `app-scc/` directory
133+
134+
| OpenJDK 25+
135+
| Leyden AOT (`-XX:AOTCache`)
136+
| `app.aot` file
137+
138+
| Older OpenJDK
139+
| AppCDS (`-XX:SharedArchiveFile`)
140+
| `app-cds.jsa` file
141+
|===
142+
143+
The detection checks `java.runtime.name` for the string `semeru`. If it matches, the SCC path is chosen. Otherwise, Quarkus checks the Java version: 25 or higher selects Leyden, anything older falls back to AppCDS.
144+
145+
If you need to override the auto-detection (for example, to force AppCDS on Semeru for comparison), you can set the type explicitly:
146+
147+
[source, properties]
148+
----
149+
quarkus.package.jar.aot.type=SCC
150+
----
151+
152+
Valid values are `AUTO` (the default), `AOT`, `AppCDS`, and `SCC`.
153+
154+
== One flag, every JVM
155+
156+
This is the design decision we are most proud of in this work. Your build configuration, CI pipeline, and Dockerfile don't need to know which JVM will run the application. Set `quarkus.package.jar.aot.enabled=true`, and the build adapts.
157+
158+
This matters in practice. Teams that standardize on Semeru for some services and OpenJDK for others can use the same Quarkus build configuration everywhere. Switching JVMs doesn't require touching `application.properties`.
159+
160+
It also means that the Quarkus documentation, guides, and examples work the same way regardless of the JVM.
161+
When we write "enable AOT for faster startup," that statement is true whether you run HotSpot, Semeru, or an older JDK.
162+
163+
== Differences from Project Leyden
164+
165+
While the user experience is intentionally identical, there are a few technical differences worth noting:
166+
167+
[cols="1,1,1", options="header"]
168+
|===
169+
| | Project Leyden (HotSpot) | Shared Classes Cache (OpenJ9)
170+
171+
| *Cache format*
172+
| Single `app.aot` file
173+
| `app-scc/` directory
174+
175+
| *What is cached*
176+
| Loaded/linked classes, method profiles (future: JIT code)
177+
| Class data, AOT-compiled native code, JIT profiling data and hints
178+
179+
| *AOT compilation*
180+
| Not yet included (planned for future JDK releases)
181+
| Included: startup methods are AOT-compiled and stored in the cache
182+
183+
| *Training*
184+
| Two-step: record configuration, then create cache in a separate JVM invocation
185+
| Single step: cache is populated during the training run itself
186+
187+
| *JVM requirement*
188+
| OpenJDK 25+
189+
| Any IBM Semeru version
190+
191+
| *Maturity*
192+
| New (JDK 25, actively evolving)
193+
| Mature (production-hardened over many years in OpenJ9)
194+
|===
195+
196+
One notable difference is that the SCC already includes AOT-compiled native code for startup methods, whereas Leyden currently caches class loading and linking data plus method profiling information, with compiled code storage planned for future JDK versions.
197+
198+
== Some numbers
199+
200+
To give you a sense of what the SCC brings in practice, we measured startup time and memory usage for the Quarkus REST JSON quickstart application on IBM Semeru 25, with and without the Shared Classes Cache.
201+
202+
[cols="2,2,1,2,1", options="header"]
203+
|===
204+
| | Startup time | Diff | RSS | Diff
205+
206+
| *Default (no SCC)*
207+
>m| 433 ms
208+
>m| Reference
209+
>m| 107 MB
210+
>m| Reference
211+
212+
| *With Shared Classes Cache*
213+
>m| 206 ms
214+
>m| -52%
215+
>m| 87 MB
216+
>m| -19%
217+
|===
218+
219+
The SCC file is `25 MB` in size — a reasonable cost for a 52% startup improvement.
220+
221+
[NOTE]
222+
====
223+
These numbers were measured on a developer laptop, not in an isolated lab environment.
224+
Your results will vary depending on your application's size and complexity.
225+
The more classes and methods your application loads during startup, the more the SCC helps.
226+
====
227+
228+
**We were able to start a Quarkus REST application on IBM Semeru in just `206 ms`** — and this is on the JVM, with full JIT compilation, full debugging support, and no native image involved.
229+
230+
== Container images and future work
231+
232+
=== Manual deployment today
233+
234+
Today, you can deploy a Quarkus application with a pre-built SCC by copying the `app-scc/` directory into your container image:
235+
236+
[source, dockerfile]
237+
----
238+
FROM icr.io/appcafe/ibm-semeru-runtimes:open-21-jre-ubi-minimal
239+
COPY target/quarkus-app /deployments
240+
WORKDIR /deployments
241+
CMD ["java", \
242+
"-Xshareclasses:name=quarkus-app,cacheDir=app-scc,readonly", \
243+
"-jar", "quarkus-run.jar"]
244+
----
245+
246+
This works, but it has a limitation: the SCC must be generated on the same JVM version that will run in the container.
247+
If your build JVM differs from the container's JVM, the cache will be ignored.
248+
To ensure a match, generate the cache using the same Semeru image in your CI pipeline.
249+
250+
=== Automated container image integration
251+
252+
The container image integration we described in the https://quarkus.io/blog/leyden-2/[Leyden post] — where Quarkus produces an AOT-optimized container image in a single command — is currently specific to the Leyden AOT path.
253+
Extending it to support SCC-based container images is on our roadmap.
254+
255+
=== Cache layering for Docker
256+
257+
This is where things get particularly interesting. OpenJ9's SCC supports https://blog.openj9.org/2019/11/05/the-multi-layer-shared-class-cache-for-docker/[multi-layer caches], a feature specifically designed for container environments.
258+
259+
The problem with a single monolithic cache in a container image is Docker's copy-on-write mechanism.
260+
If a base image contains a populated SCC and a higher layer writes to it (to add application-specific data), Docker duplicates the entire cache into the new layer.
261+
This defeats the purpose of layer sharing and inflates the image size.
262+
263+
OpenJ9 solves this with the `-Xshareclasses:createLayer` option. Instead of a single cache, you build a stack of cache layers that align with your container image layers:
264+
265+
* Layer 0 (JDK base image): cache populated with JDK and framework classes.
266+
* Layer 1 (application image): a separate, smaller cache layer containing only the application-specific class data, AOT code, and JIT hints.
267+
268+
Each cache layer is sized individually for its content, and the JVM reads from all layers at startup.
269+
Because lower layers are never modified by higher ones, Docker's layer sharing works correctly — teams using the same base image share the JDK cache layer across all their application images.
270+
271+
This is the part we are most excited about for future work. We plan to integrate this layered approach into Quarkus's container image extensions (`quarkus-container-image-docker`, `quarkus-container-image-podman`, and `quarkus-container-image-jib`).
272+
The goal is the same one-command experience we built for Leyden:
273+
274+
[source, bash]
275+
----
276+
./mvnw verify -Dquarkus.package.jar.aot.enabled=true -Dquarkus.container-image.build=true -DskipITs=false
277+
----
278+
279+
This would produce a container image with a properly layered SCC — the framework cache in a shared base layer, the application-specific cache in the top layer — optimized for both startup time and image size.
280+
281+
In microservices architectures where multiple Quarkus services share the same base image and dependency set, this layering could significantly reduce the total storage and transfer cost across all service images.
282+
283+
== Acknowledgements
284+
285+
We would like to thank the OpenJDK team at IBM for their collaboration. The discussions around Leyden and Semeru's shared classes technology were instrumental in shaping this integration.
286+
287+
== Conclusion
288+
289+
The beauty of this integration is its simplicity: one flag, multiple JVMs, optimal performance everywhere. Whether you're running OpenJDK or IBM Semeru, Quarkus has you covered, as Quarkus has always been about meeting developers where they are.
290+
291+
We will continue tracking developments in both Project Leyden and OpenJ9 to bring you the best performance on whatever platform you choose.
292+
293+
== Come Join Us
294+
295+
We value your feedback a lot so please report bugs, ask for improvements... Let's build something great together!
296+
297+
If you are a Quarkus user or just curious, don't be shy and join our welcoming community:
298+
299+
* provide feedback on https://github.com/quarkusio/quarkus/issues[GitHub];
300+
* craft some code and https://github.com/quarkusio/quarkus/pulls[push a PR];
301+
* discuss with us on https://quarkusio.zulipchat.com/[Zulip] and on the https://groups.google.com/d/forum/quarkus-dev[mailing list];
302+
* ask your questions on https://stackoverflow.com/questions/tagged/quarkus[Stack Overflow].

0 commit comments

Comments
 (0)