DEV Community

Cover image for Quarkus Native vs JVM: Real-World Performance Comparison
issam1991
issam1991

Posted on

Quarkus Native vs JVM: Real-World Performance Comparison

Discover the real-world performance differences between Quarkus Native Image and Quarkus JVM. Compare build times, Docker image sizes, startup times, and memory usage with actual measurements from a production-ready application. Make informed decisions about which deployment option suits your use case.


Introduction

Quarkus offers two primary deployment options: Native Image (compiled ahead-of-time with GraalVM) and Traditional JVM (running on the Java Virtual Machine). Both have their strengths, but choosing the right one depends on your specific requirements.

This article presents a comprehensive, real-world comparison using the same Quarkus application built and deployed in both modes. We'll measure:

  • Build Time: How long it takes to compile and package
  • Docker Image Size: Final container image size
  • Startup Time: Application startup time (from initialization to ready state, measured by Quarkus)
  • Memory Usage: At startup and under load
  • Build Artifact Size: Native executable vs JAR file

Methodology

All measurements were taken using:

  • Application: User Management System (Quarkus 3.15.1 + Java 21)
  • Database: MariaDB 12
  • Container Runtime: Docker
  • Startup Time Measurement: Quarkus application logs (internal measurement)
  • Other Measurements: Docker stats and build tools
  • Test Environment: Windows 11, Docker Desktop, 8GB RAM allocated to Docker

The application includes:

  • REST API with JAX-RS
  • Hibernate ORM Panache for data access
  • OpenAPI/Swagger documentation
  • Database migrations with Flyway

Build Time Comparison

Native Image Build

Building a Quarkus Native Image involves ahead-of-time compilation, which is computationally intensive:

cd quarkus
./mvnw -Pnative -Dquarkus.native.container-build=true clean package
Enter fullscreen mode Exit fullscreen mode

Results:

  • Build Time: ~210 seconds (3.5 minutes)
  • Process: Compiles Java bytecode to native machine code
  • Requirements: GraalVM native-image compiler (runs in Docker container)

JVM Build

Building for JVM is a standard Java compilation process:

cd quarkus
./mvnw -Pjvm clean package
Enter fullscreen mode Exit fullscreen mode

Results:

  • Build Time: ~15 seconds
  • Process: Compiles Java to bytecode, packages into JAR
  • Requirements: Standard JDK

Build Time Analysis

Metric Native Image JVM Difference
Build Time ~210 seconds ~15 seconds 195 seconds (1300% slower)
Build Complexity High (AOT compilation) Low (Standard compilation) -

Key Observations:

  • Native Image builds are significantly slower due to AOT compilation
  • JVM builds are faster and more familiar to Java developers
  • Native builds benefit from Docker layer caching

Docker Image Size Comparison

Native Image Dockerfile

FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
COPY target/*-runner /work/application/application
EXPOSE 8080
ENTRYPOINT ["/work/application/application"]
Enter fullscreen mode Exit fullscreen mode

Results:

  • Image Size: 123 MB
  • Base Image: quarkus-micro-image:2.0 (minimal UBI-based image)
  • Contents: Native executable only

JVM Dockerfile

FROM eclipse-temurin:21-jre-alpine
WORKDIR /work/
COPY target/quarkus-app /work/application
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/work/application/quarkus-run.jar"]
Enter fullscreen mode Exit fullscreen mode

Results:

  • Image Size: 254 MB
  • Base Image: eclipse-temurin:21-jre-alpine (Alpine Linux with JRE)
  • Contents: JAR file + JRE

Image Size Analysis

Metric Native Image JVM Difference
Docker Image Size 123 MB 254 MB 131 MB (107% larger for JVM)
Base Image Minimal UBI Alpine + JRE -
Application Artifact Native executable JAR + dependencies -

Key Observations:

  • Native images are typically smaller (no JRE included)
  • JVM images include the full JRE runtime
  • Alpine base images help reduce JVM image size

Startup Time Comparison

Native Image Startup

Native executables start almost instantly because:

  • No JVM initialization
  • No class loading
  • No JIT compilation
  • Pre-compiled machine code

Results:

  • Startup Time: 0.079 s (79 ms) (from Quarkus application logs)

Actual Log Output:

quarkus-app  | 2025-11-16 23:09:36,900 INFO  [io.quarkus] (main) quarkus-native-users 1.0.0-SNAPSHOT native (powered by Quarkus 3.15.1) started in 0.079s. Listening on: http://0.0.0.0:8080
Enter fullscreen mode Exit fullscreen mode

JVM Startup

JVM applications take longer to start because:

  • JVM initialization
  • Class loading
  • JIT compilation (happens at runtime)
  • Warmup period

Results:

  • Startup Time: 2.292 s (from Quarkus application logs)

Actual Log Output:

quarkus-jvm-app | 2025-11-16 23:11:45,553 INFO  [io.quarkus] (main) quarkus-native-users 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.15.1) started in 2.292s. Listening on: http://0.0.0.0:8080
Enter fullscreen mode Exit fullscreen mode

Note: These startup times are measured by Quarkus itself and represent the actual application startup time (from application initialization to ready state), which is more accurate than external measurements that include container startup overhead.

Startup Time Analysis

Metric Native Image JVM Difference
Startup Time 0.079 s (79 ms) 2.292 s 2.213 s (~29x faster for Native)
Cold Start Instant Requires warmup -
First Request Fast Slower (JIT not warmed up) -

Key Observations:

  • Native Image starts significantly faster (often 10-100x faster)
  • JVM requires warmup time for optimal performance
  • Native Image is ideal for serverless and short-lived containers

Memory Usage Comparison

Memory at Startup

Memory usage immediately after application startup:

Native Image:

  • Memory Usage: 16.27 MiB (at startup, measured from Docker stats)
  • Container Limit: 64 MiB (25.43% utilization)
  • Components: Native executable, minimal runtime

JVM:

  • Memory Usage: 113.5 MiB (at startup, measured from Docker stats)
  • Container Limit: 128 MiB (88.69% utilization)
  • Components: JVM heap, metaspace, code cache, JIT compiler

Real Docker Stats Output (JVM at Startup):

CONTAINER ID   NAME               MEM USAGE / LIMIT     MEM %
40292f4fa0a6   quarkus-jvm-app    113.5MiB / 128MiB     88.69%
Enter fullscreen mode Exit fullscreen mode

Real Docker Stats Output (Native at Startup):

CONTAINER ID   NAME               MEM USAGE / LIMIT     MEM %
435d098f0299   quarkus-app        16.27MiB / 64MiB      25.43%
Enter fullscreen mode Exit fullscreen mode

Memory Under Load

Memory usage after handling API requests:

Native Image:

  • Memory Usage: 26.71 MiB (under load, after creating users)
  • Container Limit: 64 MiB (41.74% utilization)
  • Growth: ~10.44 MiB from startup (from 16.27 MiB baseline)

Real Docker Stats Output (Native Under Load):

CONTAINER ID   NAME               MEM USAGE / LIMIT     MEM %
435d098f0299   quarkus-app        26.71MiB / 64MiB      41.74%
Enter fullscreen mode Exit fullscreen mode

JVM:

  • Memory Usage: 122.6 MiB (under load, after creating users)
  • Container Limit: 128 MiB (95.76% utilization)
  • Growth: ~9.1 MiB from startup (from 113.5 MiB baseline)

Real Docker Stats Output (JVM Under Load):

CONTAINER ID   NAME               MEM USAGE / LIMIT     MEM %
40292f4fa0a6   quarkus-jvm-app    122.6MiB / 128MiB    95.76%
Enter fullscreen mode Exit fullscreen mode

Memory Analysis

Metric Native Image JVM Difference
Memory at Startup 16.27 MiB 113.5 MiB 97.23 MiB (~86% less for Native)
Memory under Load 26.71 MiB 122.6 MiB 95.89 MiB (~78% less for Native)
Memory Growth ~10.44 MiB ~9.1 MiB -

Key Observations:

  • Native Image uses 86% less memory at startup (16.27 MiB vs 113.5 MiB)
  • Under load, Native uses 78% less memory (26.71 MiB vs 122.6 MiB)
  • Native Image grows by ~10.44 MiB under load, while JVM grows by ~9.1 MiB
  • JVM has significant overhead from JIT compiler, code cache, and runtime structures
  • Native Image uses only 25.43% of its 64MB limit at startup and 41.74% under load
  • JVM uses 88.69% of its 128MB limit at startup and 95.76% under load
  • JVM reaches 95.76% of its 128MB limit under load, leaving minimal headroom
  • Native Image maintains significant headroom (58.26% available) even under load
  • Native Image is ideal for memory-constrained environments

Build Artifact Size

Native Executable

The compiled native executable:

  • Size: ~98.65 MB (native executable)
  • Location: target/*-runner
  • Type: Platform-specific binary

JVM JAR

The packaged JAR file:

  • Size: ~48.67 MB (quarkus-app directory total)
  • Location: target/quarkus-app/quarkus-run.jar
  • Type: Java bytecode archive

Artifact Size Analysis

Metric Native Executable JVM JAR Notes
Size ~99 MB ~49 MB Native executable is larger but includes everything
Portability Platform-specific Cross-platform JAR runs on any JVM

Performance Summary

Quick Comparison Table

Metric Native Image JVM Winner
Build Time ~210s (3.5 min) ~15s JVM (14x faster)
Docker Image Size 123 MB 254 MB Native (52% smaller)
Startup Time 0.079 s (79 ms) 2.292 s Native (~29x faster)
Memory (Startup) 16.27 MiB 113.5 MiB Native (86% less)
Memory (Load) 26.71 MiB 122.6 MiB Native (78% less)
Runtime Performance Good Excellent (after warmup) JVM (long-term)

When to Use Native Image

Choose Native Image when:

Fast startup is critical

  • Serverless functions (AWS Lambda, Azure Functions)
  • Microservices with frequent scaling
  • Short-lived containers
  • Kubernetes with aggressive scaling policies

Memory is constrained

  • Edge computing devices
  • Resource-limited environments
  • High-density deployments

Cold start performance matters

  • Applications with infrequent traffic
  • Batch processing jobs
  • Scheduled tasks

Smaller deployments

  • Reduced image sizes
  • Faster container pulls
  • Lower storage costs

When to Use JVM

Choose JVM when:

Build time is a concern

  • Frequent deployments
  • CI/CD pipeline speed matters
  • Development iterations

Maximum runtime performance needed

  • Long-running applications
  • High-throughput systems
  • After JIT warmup, JVM can outperform native

Easier debugging and profiling

  • Standard Java tooling works
  • JVM profilers (JProfiler, VisualVM)
  • Better reflection and dynamic features

Complex reflection/dynamic code

  • Frameworks with heavy reflection
  • Dynamic class loading
  • Some libraries not fully native-compatible

Configuration Examples

Building Native Image

cd quarkus
./mvnw -Pnative -Dquarkus.native.container-build=true \
  -Dquarkus.container-image.build=true \
  -Dquarkus.container-image.name=quarkus-native-users \
  -Dquarkus.container-image.tag=latest \
  clean package
Enter fullscreen mode Exit fullscreen mode

Building JVM Image

cd quarkus
./mvnw -Pjvm -Dquarkus.container-image.build=true \
  -Dquarkus.container-image.name=quarkus-jvm-users \
  -Dquarkus.container-image.tag=latest \
  clean package
Enter fullscreen mode Exit fullscreen mode

Docker Compose

Native:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

JVM:

docker-compose -f docker-compose.jvm.yml up -d
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

Serverless Functions

Native Image is ideal for serverless:

  • Ultra-fast cold starts
  • Minimal memory footprint
  • Cost-effective (pay per invocation)

Microservices

Both can work, but consider:

  • Native: If you need fast scaling and low memory
  • JVM: If you have long-running services and need maximum throughput

Batch Processing

Native Image excels for:

  • Short-lived batch jobs
  • Scheduled tasks
  • One-time processing

Traditional Web Applications

JVM is often better for:

  • Long-running applications
  • High-traffic websites
  • Applications benefiting from JIT optimization

Conclusion

Both Quarkus Native Image and JVM have their place in modern Java development:

  • Native Image wins on startup time, memory usage, and image size
  • JVM wins on build time, long-term performance (after warmup), and developer experience

The choice depends on your specific requirements:

  • Choose Native Image for serverless, microservices with frequent scaling, and memory-constrained environments
  • Choose JVM for traditional applications, maximum runtime performance, and faster development cycles

Key Takeaways

  1. Native Image offers ~29x faster startup (0.079s vs 2.292s) and 86% less memory at startup (16.27 MiB vs 113.5 MiB)
  2. JVM offers 14x faster builds (~15s vs ~210s) and better long-term performance after warmup
  3. Native Image produces 52% smaller Docker images (123MB vs 254MB)
  4. Both are production-ready and well-supported by Quarkus
  5. Measure your specific use case to make the best decision

Resources


Follow me on GitHub and connect with ForTek Advisor for more technical content and project updates.

Originally published at https://fortek-advisor.com on November 17, 2025.

Top comments (0)