Packaging and JVM Modes
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Running a Quarkus application on the JVM is not a single, fixed artifact shape — ./mvnw package can produce
several structurally different outputs depending on quarkus.package.jar.type, each trading off startup speed,
distribution simplicity, and container-image size differently. This page covers those JVM package types and
their quarkus.package.* configuration, then two build-time techniques that shrink the JVM’s own startup and
memory cost further — AOT caching and dependency tree-shaking — plus JLink custom runtime images as an
alternative to a full JRE base image. It closes with build reproducibility: CycloneDX SBOM generation, releasing
the resulting artifacts, and a decision table comparing JVM fast-jar, a JLink runtime, and native image head to
head. Native image’s own build process and the closed-world assumption both package types rely on are covered in
Build-Time Augmentation and the Closed World; this page only compares native image’s operating
characteristics against the JVM options, it does not re-explain how native-image itself works.
Package types
quarkus.package.jar.type selects which of five structurally different JAR layouts ./mvnw package produces.
Only one can be active per build:
| Type | Artifact shape | When to use it |
|---|---|---|
fast-jar (default) |
A |
The default for a reason: fastest JVM startup of the JAR types, and the shape every "Getting Started" guide and container-image extension assumes. |
legacy-jar |
A single |
Pre-1.12 Quarkus layout, kept for tooling or deployment pipelines that specifically expect it. |
uber-jar |
One self-contained |
Distributing a single file matters more than startup time or a minimal image layer — copying one artifact into a scratch container, or handing a jar to something that cannot unpack a directory tree. |
mutable-jar |
A |
Iterative, production-like testing where a change should re-augment without a full clean build. Fully covered, alongside re-augmentation itself, in Build-Time Augmentation and the Closed World; not re-explained here. |
aot-jar |
A |
Paired with AOT caching below rather than chosen on its own — see that section. |
The fast-jar layout
Because fast-jar is the default, its directory shape is worth naming explicitly. ./mvnw package writes it to
target/quarkus-app/:
| Path | Contents |
|---|---|
|
The thin, executable entry-point JAR — |
|
Third-party dependency JARs, split into |
|
The application’s own compiled classes and resources — what |
|
Generated, augmentation-produced classes: the bytecode that |
Splitting the layout this way is what makes fast-jar fast to start: the JVM can memory-map an already-indexed,
already-separated set of directories instead of opening and scanning a single large archive, and a container
image can cache the rarely-changing lib/ layer separately from the frequently-changing app/ layer.
Configuration
The properties below are the ones most builds actually touch; quarkus.package.jar.* has several more
narrowly-scoped options (manifest sections, optional-dependency filtering) covered in full in the Maven Tooling
guide linked in References.
| Property | Default | Purpose |
|---|---|---|
|
|
Selects the package type: |
|
|
Where the final artifact or |
|
The Maven/Gradle artifact id |
Base file name for the produced artifact, without suffix or extension. |
|
|
Whether entries inside the produced JAR(s) are compressed; disabling it trades a larger artifact for a faster build. |
|
|
Appends |
|
|
Decompiles the augmentation-generated bytecode in |
A complete example
src/main/resources/application.properties:
# Base package type -- overridden per-build below where needed.
quarkus.package.jar.type=fast-jar
# Keep the artifact name stable regardless of the Maven version qualifier.
quarkus.package.output-name=inventory-service
# Record a manifest attribute useful for support tickets.
quarkus.package.jar.manifest.attributes."Build-Vendor"=Irurueta
Building the default fast-jar and overriding the type for a one-off uber-jar build both work from the same project, with no other configuration change:
# Default build -- produces target/quarkus-app/ (fast-jar).
./mvnw package
# One-off override -- produces target/inventory-service-runner.jar (uber-jar), leaving
# application.properties untouched.
./mvnw package -Dquarkus.package.jar.type=uber-jar
The first command leaves target/quarkus-app/quarkus-run.jar (plus its lib/, app/, quarkus/ siblings) as
the runnable artifact, alongside the plain target/inventory-service-1.0.0-SNAPSHOT.jar that Maven’s own jar
plugin always produces first and that fast-jar’s build step repackages from. The second command instead leaves a
single target/inventory-service-runner.jar containing every dependency’s unpacked classes — nothing under
target/quarkus-app/ is produced for that build.
AOT caching, tree-shaking, and JLink custom runtime images
Three further, independent techniques reduce what the JVM has to do at every process start, on top of the package-type choice above.
AOT caching
Starting with JDK 24, Project Leyden’s AOT cache lets a JVM process record the classes it loaded and the code
the JIT compiled during a representative run, then load that recorded cache on every later start instead of
repeating class loading, verification and (re-)JIT-warmup from scratch — narrowing, on the JVM, part of the
startup-time gap that has historically favored native image. Quarkus wires this up through the aot-jar package
type described above: enabling quarkus.package.jar.aot.enabled=true (via -D on the Maven build, or in
application.properties) produces a quarkus-app/ tree structured so the JDK’s own AOT-cache tooling can
consume it, and quarkus.package.jar.aot.type selects the cache format (auto, aot, app-cds, or scc,
with app-cds as the pre-JDK-24 fallback using the older Application Class-Data Sharing mechanism). The cache
itself is trained — built from an actual run of the application, ideally exercising its real request paths — which quarkus.package.jar.aot.phase lets you trigger either during the build or during the project’s own
integration tests, so the shipped cache reflects genuine startup and warm-up behavior rather than an empty run.
The resulting application still starts with java -jar quarkus-run.jar, but pointed at the cache with an added
flag, e.g. -XX:AOTCache=app.aot. Because the cache is tied to the exact JDK build that produced it, it must be
regenerated whenever the runtime JDK changes.
Tree-shaking
Fast-jar’s lib/ directory does not automatically contain only the classes the application actually reaches — by default it contains every class in every declared dependency’s JAR, whether augmentation’s closed-world
analysis ever found a path to it or not. quarkus.package.jar.tree-shake.mode turns on tree-shaking: dropping
classes from bundled dependency JARs that augmentation determined are unreachable from the application’s own
closed-world graph, shrinking both the artifact and the amount the JVM has to index at startup.
quarkus.package.jar.tree-shake.excluded-artifacts exempts specific dependency coordinates from shaking — for a
library that loads a class reflectively in a way augmentation’s analysis cannot see, tree-shaking it away would
silently break that reflective lookup at runtime, so exclude it explicitly rather than disabling tree-shaking
project-wide.
JLink custom runtime images
A standard JVM deployment bundles the application against a full JRE — every module the JDK ships, whether the
application uses it or not. The JDK’s own jlink tool builds a custom runtime image instead: a
purpose-built mini-JRE containing only the JDK modules (plus, via Quarkus’s quarkus-jlink extension,
the application’s own modules) the application actually needs, alongside a generated launcher script. The result
is smaller to store and ship, faster to start because there is less runtime surface to initialize, and more
self-contained than depending on whatever JRE image a target host or base container happens to provide.
quarkus.jlink.enabled turns image generation on (Maven/Gradle build this image into
quarkus.jlink.output-directory, target/jlink-output by default); quarkus.jlink.image-path and
quarkus.jlink.launcher-name control where the image and its launcher land, and quarkus.jlink.min-heap-size /
quarkus.jlink.max-heap-size bake default heap flags into that launcher. As of this writing the extension needs
Java 25 or later and is marked experimental in the upstream guide — worth trying for a leaner JVM-mode container
base image, but not yet something to depend on as a stable, unchanging build step.
Build reproducibility
CycloneDX SBOM generation
Adding io.quarkus:quarkus-cyclonedx as a project dependency makes every subsequent ./mvnw package emit a
CycloneDX-format Software Bill of Materials for the packaged application
automatically — no extra goal invocation is required, and no opt-in flag needs setting once the dependency is
present. The
distribution SBOM lands next to the built artifact as <executable-name>-cyclonedx.json (the format is
configurable via quarkus.cyclonedx.format, e.g. xml); a separate quarkus:dependency-sbom Maven goal can
additionally record the dependency graph before packaging, written as
target/<artifactId>-<version>-dependency-cyclonedx.json. Either SBOM is what a vulnerability scanner or a
supply-chain policy consumes — it lists every artifact, with its coordinates and (where known) license, that
ended up in the shipped output.
Releasing the artifacts
Packaging and SBOM generation produce the artifacts; getting them from target/ to a GitHub Release, a
container registry, and a changelog entry is a separate concern that Quarkus itself has no opinion on. JReleaser
is a general-purpose Java release-automation tool that fits well here: pointed at the JARs (or container images)
./mvnw package already produced, it can sign them, publish to Maven Central or a registry, generate release
notes, and cut a GitHub/GitLab release, all driven from one declarative configuration file kept alongside the
project rather than a hand-rolled release script.
Choosing a JVM/native strategy
The package types and techniques above, plus native image (covered in Build-Time Augmentation and the Closed World), sit at genuinely different points on the same trade-off curve:
| Strategy | Startup time | Memory (RSS) | Build time | Portability | Tooling maturity |
|---|---|---|---|---|---|
JVM fast-jar |
Moderate — milliseconds to a few seconds |
Moderate — full JRE resident |
Fastest — no AOT compilation step |
Highest — runs on any conforming JVM, any OS/arch with one installed |
Most mature — the default path every guide and extension targets |
JVM + JLink custom runtime |
Similar to fast-jar, slightly faster class loading |
Lower than a full JRE — unused modules dropped |
Slightly slower than plain fast-jar — an extra |
Lower — the image is tied to the OS/arch it was linked for, unlike a portable JAR |
Experimental — requires Java 25+, upstream guide still marked experimental |
Native image |
Fastest — tens of milliseconds, no JIT warm-up |
Lowest — no JVM, no JIT, ahead-of-time-compiled machine code only |
Slowest — GraalVM reachability analysis and AOT compilation are heavyweight |
Lowest — one executable per target OS/arch, closed-world restrictions apply to every dependency |
Maturing fast, but still the option most likely to need per-library reflection configuration |
No single row is a universal answer: fast-jar remains the safest default for anything still stabilizing its dependency set, native image is worth its build-time and dependency-compatibility cost for latency- or density-sensitive deployments (serverless, high pod-count Kubernetes), and a JLink runtime is a middle ground worth revisiting as it matures, for JVM deployments that specifically want a smaller, more self-contained image without native image’s closed-world constraints.