Quarkus Native vs JVM Mode: Comprehensive Comparison

Quarkus offers two primary execution modes: JVM Mode (traditional Java Virtual Machine) and Native Mode (compiled to native code via GraalVM). Understanding the differences is crucial for choosing the right approach for your application.

Overview and Setup

Dependencies Configuration

<!-- pom.xml - Quarkus Dependencies -->
<properties>
<quarkus.platform.version>3.8.0</quarkus.platform.version>
</properties>
<dependencies>
<!-- Core Quarkus -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Sample Application for Comparison

Example 1: REST API with Database Access

// Entity
package com.example.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
public class Product extends PanacheEntity {
@NotBlank
@Size(min = 3, max = 100)
public String name;
public String description;
public Double price;
public Integer stockQuantity;
// Constructors
public Product() {}
public Product(String name, String description, Double price, Integer stockQuantity) {
this.name = name;
this.description = description;
this.price = price;
this.stockQuantity = stockQuantity;
}
}
// Repository
package com.example.repository;
import com.example.entity.Product;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
public List<Product> findByName(String name) {
return find("name like ?1", "%" + name + "%").list();
}
public List<Product> findExpensiveProducts(Double minPrice) {
return find("price >= ?1", minPrice).list();
}
public long countInStock() {
return find("stockQuantity > 0").count();
}
}
// REST Resource
package com.example.resource;
import com.example.entity.Product;
import com.example.repository.ProductRepository;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@Inject
ProductRepository productRepository;
@GET
public List<Product> getAllProducts() {
return productRepository.listAll();
}
@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
Product product = productRepository.findById(id);
if (product == null) {
throw new WebApplicationException("Product not found", 404);
}
return product;
}
@POST
@Transactional
public Response createProduct(@Valid Product product) {
productRepository.persist(product);
return Response.status(Response.Status.CREATED).entity(product).build();
}
@PUT
@Path("/{id}")
@Transactional
public Product updateProduct(@PathParam("id") Long id, @Valid Product productUpdate) {
Product product = productRepository.findById(id);
if (product == null) {
throw new WebApplicationException("Product not found", 404);
}
product.name = productUpdate.name;
product.description = productUpdate.description;
product.price = productUpdate.price;
product.stockQuantity = productUpdate.stockQuantity;
return product;
}
@GET
@Path("/search")
public List<Product> searchProducts(@QueryParam("name") String name) {
return productRepository.findByName(name);
}
@GET
@Path("/expensive")
public List<Product> getExpensiveProducts(@QueryParam("minPrice") Double minPrice) {
return productRepository.findExpensiveProducts(minPrice != null ? minPrice : 100.0);
}
}

Example 2: Configuration and Additional Services

// Configuration class
package com.example.config;
import io.smallrye.config.ConfigMapping;
import java.util.Optional;
@ConfigMapping(prefix = "app")
public interface AppConfig {
String name();
Optional<String> description();
CacheConfig cache();
interface CacheConfig {
boolean enabled();
int ttl();
int maxSize();
}
}
// Caching service
package com.example.service;
import com.example.config.AppConfig;
import io.quarkus.cache.CacheResult;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.time.LocalDateTime;
@ApplicationScoped
public class ProductService {
@Inject
AppConfig appConfig;
@CacheResult(cacheName = "product-cache")
public String getProductInfoWithCache(Long productId) {
// Simulate expensive operation
simulateProcessing();
return "Product info for ID: " + productId + " at " + LocalDateTime.now();
}
private void simulateProcessing() {
try {
Thread.sleep(100); // Simulate processing time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public boolean isCacheEnabled() {
return appConfig.cache().enabled();
}
}
// Scheduled task
package com.example.task;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
@ApplicationScoped
public class MaintenanceTask {
private static final Logger LOG = Logger.getLogger(MaintenanceTask.class);
@Inject
ProductCleanupService cleanupService;
@Scheduled(every = "1h")
void cleanupOldProducts() {
if (cleanupService.isEnabled()) {
LOG.info("Starting scheduled product cleanup");
int cleaned = cleanupService.cleanupOldProducts();
LOG.infof("Cleaned up %d old products", cleaned);
}
}
@Scheduled(every = "5m")
void healthCheck() {
LOG.debug("Performing health check");
// Health check logic
}
}

Performance Comparison Tests

Example 3: Performance Benchmarking

package com.example.benchmark;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PerformanceComparisonTest {
private static final int WARMUP_ITERATIONS = 100;
private static final int MEASUREMENT_ITERATIONS = 1000;
@Test
@Order(1)
@EnabledIfSystemProperty(named = "quarkus.package.type", matches = "jar")
public void testJvmModePerformance() {
System.out.println("=== JVM MODE PERFORMANCE TEST ===");
runPerformanceTests();
}
@Test
@Order(2)
@EnabledIfSystemProperty(named = "quarkus.package.type", matches = "native")
public void testNativeModePerformance() {
System.out.println("=== NATIVE MODE PERFORMANCE TEST ===");
runPerformanceTests();
}
private void runPerformanceTests() {
// Warmup phase
for (int i = 0; i < WARMUP_ITERATIONS; i++) {
performGetRequest();
performPostRequest();
}
// GET request performance
long getStartTime = System.currentTimeMillis();
for (int i = 0; i < MEASUREMENT_ITERATIONS; i++) {
performGetRequest();
}
long getEndTime = System.currentTimeMillis();
// POST request performance
long postStartTime = System.currentTimeMillis();
for (int i = 0; i < MEASUREMENT_ITERATIONS; i++) {
performPostRequest();
}
long postEndTime = System.currentTimeMillis();
System.out.printf("GET Requests: %d requests in %d ms (avg: %.2f ms/req)%n",
MEASUREMENT_ITERATIONS,
getEndTime - getStartTime,
(double)(getEndTime - getStartTime) / MEASUREMENT_ITERATIONS);
System.out.printf("POST Requests: %d requests in %d ms (avg: %.2f ms/req)%n",
MEASUREMENT_ITERATIONS,
postEndTime - postStartTime,
(double)(postEndTime - postStartTime) / MEASUREMENT_ITERATIONS);
}
private void performGetRequest() {
given()
.when().get("/products")
.then()
.statusCode(200);
}
private void performPostRequest() {
String productJson = """
{
"name": "Test Product",
"description": "Performance test product",
"price": 99.99,
"stockQuantity": 10
}
""";
given()
.contentType(ContentType.JSON)
.body(productJson)
.when().post("/products")
.then()
.statusCode(201);
}
@Test
public void testMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
System.out.printf("Memory Usage: Used=%d MB, Max=%d MB%n",
usedMemory / (1024 * 1024),
maxMemory / (1024 * 1024));
}
@Test
public void testStartupTime() {
// This would typically be measured during application startup
// For demonstration, we'll simulate startup time measurement
long simulatedStartupTime = simulateStartupMeasurement();
System.out.printf("Simulated Startup Time: %d ms%n", simulatedStartupTime);
}
private long simulateStartupMeasurement() {
// In real scenarios, this would measure actual startup time
// For testing purposes, we return a simulated value
String packageType = System.getProperty("quarkus.package.type", "jar");
return "native".equals(packageType) ? 50 : 2000; // Native starts faster
}
}

Build and Deployment Configuration

Example 4: Docker and Build Configuration

# Dockerfile.jvm - For JVM Mode
FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to display heap size
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY target/quarkus-app/lib/ /deployments/lib/
COPY target/quarkus-app/*.jar /deployments/
COPY target/quarkus-app/app/ /deployments/app/
COPY target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ]
# Dockerfile.native - For Native Mode
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
# application.properties - Configuration for both modes
# Application config
app.name=Product Service
app.description=Quarkus Product Management Service
app.cache.enabled=true
app.cache.ttl=3600
app.cache.max-size=1000
# Database configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkusdb
# Hibernate configuration
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
# HTTP configuration
quarkus.http.port=8080
quarkus.http.host=0.0.0.0
# Logging
quarkus.log.level=INFO
quarkus.log.category."com.example".level=DEBUG
# Native-specific configuration
quarkus.native.additional-build-args=--initialize-at-build-time=org.slf4j.MDC\
--initialize-at-build-time=org.slf4j.LoggerFactory\
--initialize-at-build-time=org.slf4j.impl.StaticLoggerBinder
# JVM-specific configuration
quarkus.jvm.initial-ram-percentage=50.0
quarkus.jvm.max-ram-percentage=75.0

Build Commands and Execution

Example 5: Build Scripts and Commands

#!/bin/bash
# build-and-compare.sh
echo "=== Quarkus JVM vs Native Mode Comparison ==="
# Build JVM version
echo "Building JVM version..."
./mvnw clean package -DskipTests
# Build Native version
echo "Building Native version..."
./mvnw clean package -Pnative -DskipTests
# Display build sizes
JVM_SIZE=$(du -h target/quarkus-app | tail -n1 | cut -f1)
NATIVE_SIZE=$(du -h target/*-runner | tail -n1 | cut -f1)
echo "=== Build Results ==="
echo "JVM Mode size: $JVM_SIZE"
echo "Native Mode size: $NATIVE_SIZE"
# Startup time comparison
echo ""
echo "=== Startup Time Comparison ==="
echo "Starting JVM application..."
time java -jar target/quarkus-app/quarkus-run.jar &
JVM_PID=$!
sleep 10  # Wait for startup
kill $JVM_PID
echo ""
echo "Starting Native application..."
time ./target/*-runner &
NATIVE_PID=$!
sleep 5  # Native starts faster
kill $NATIVE_PID
<!-- pom.xml - Build Profiles -->
<profiles>
<profile>
<id>native</id>
<properties>
<quarkus.package.type>native</quarkus.package.type>
<quarkus.native.container-build>true</quarkus.native.container-build>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<configuration>
<enableHttpUrlHandler>true</enableHttpUrlHandler>
<enableHttpsUrlHandler>true</enableHttpsUrlHandler>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

Comprehensive Comparison Results

Example 6: Comparison Analysis

package com.example.analysis;
import java.util.*;
public class QuarkusComparisonAnalysis {
public static class ComparisonMetrics {
public final String mode;
public final long startupTimeMs;
public final long buildTimeMs;
public final long executableSizeBytes;
public final long memoryUsageMb;
public final double requestsPerSecond;
public final double averageResponseTimeMs;
public ComparisonMetrics(String mode, long startupTimeMs, long buildTimeMs,
long executableSizeBytes, long memoryUsageMb,
double requestsPerSecond, double averageResponseTimeMs) {
this.mode = mode;
this.startupTimeMs = startupTimeMs;
this.buildTimeMs = buildTimeMs;
this.executableSizeBytes = executableSizeBytes;
this.memoryUsageMb = memoryUsageMb;
this.requestsPerSecond = requestsPerSecond;
this.averageResponseTimeMs = averageResponseTimeMs;
}
public void printComparison(ComparisonMetrics other) {
System.out.println("=== QUARKUS " + mode + " vs " + other.mode + " MODE COMPARISON ===");
System.out.printf("Startup Time: %d ms vs %d ms (%.2fx %s)%n",
startupTimeMs, other.startupTimeMs,
(double) other.startupTimeMs / startupTimeMs,
startupTimeMs < other.startupTimeMs ? "faster" : "slower");
System.out.printf("Build Time: %d ms vs %d ms (%.2fx %s)%n",
buildTimeMs, other.buildTimeMs,
(double) other.buildTimeMs / buildTimeMs,
buildTimeMs < other.buildTimeMs ? "faster" : "slower");
System.out.printf("Executable Size: %d MB vs %d MB (%.2fx %s)%n",
executableSizeBytes / (1024 * 1024),
other.executableSizeBytes / (1024 * 1024),
(double) other.executableSizeBytes / executableSizeBytes,
executableSizeBytes < other.executableSizeBytes ? "smaller" : "larger");
System.out.printf("Memory Usage: %d MB vs %d MB (%.2fx %s)%n",
memoryUsageMb, other.memoryUsageMb,
(double) other.memoryUsageMb / memoryUsageMb,
memoryUsageMb < other.memoryUsageMb ? "less" : "more");
System.out.printf("Throughput: %.2f req/s vs %.2f req/s (%.2fx %s)%n",
requestsPerSecond, other.requestsPerSecond,
requestsPerSecond / other.requestsPerSecond,
requestsPerSecond > other.requestsPerSecond ? "higher" : "lower");
System.out.printf("Response Time: %.2f ms vs %.2f ms (%.2fx %s)%n",
averageResponseTimeMs, other.averageResponseTimeMs,
other.averageResponseTimeMs / averageResponseTimeMs,
averageResponseTimeMs < other.averageResponseTimeMs ? "faster" : "slower");
}
}
public static void main(String[] args) {
// Sample metrics (real values would come from actual measurements)
ComparisonMetrics jvmMetrics = new ComparisonMetrics(
"JVM",
2000,    // startup time
30000,   // build time
150 * 1024 * 1024, // executable size (JAR + JVM)
256,     // memory usage
850.5,   // requests per second
12.3     // average response time
);
ComparisonMetrics nativeMetrics = new ComparisonMetrics(
"NATIVE", 
50,      // startup time
120000,  // build time
80 * 1024 * 1024,  // executable size
45,      // memory usage  
920.2,   // requests per second
10.8     // average response time
);
// Print comparison
nativeMetrics.printComparison(jvmMetrics);
// Print recommendations
printRecommendations(jvmMetrics, nativeMetrics);
}
public static void printRecommendations(ComparisonMetrics jvm, ComparisonMetrics nativeMetrics) {
System.out.println("\n=== RECOMMENDATIONS ===");
if (nativeMetrics.startupTimeMs < jvm.startupTimeMs / 10) {
System.out.println("✅ Use NATIVE mode for serverless/FaaS deployments");
}
if (nativeMetrics.memoryUsageMb < jvm.memoryUsageMb / 2) {
System.out.println("✅ Use NATIVE mode for memory-constrained environments");
}
if (jvm.buildTimeMs < nativeMetrics.buildTimeMs / 3) {
System.out.println("✅ Use JVM mode for development and rapid iteration");
}
if (nativeMetrics.requestsPerSecond > jvm.requestsPerSecond * 1.1) {
System.out.println("✅ Use NATIVE mode for high-throughput applications");
}
if (jvm.averageResponseTimeMs < nativeMetrics.averageResponseTimeMs) {
System.out.println("⚠️  JVM mode may have better peak performance for some workloads");
}
System.out.println("\n=== GENERAL GUIDELINES ===");
System.out.println("• Use JVM mode for: Development, CI/CD, applications with dynamic class loading");
System.out.println("• Use Native mode for: Production, microservices, serverless, resource-constrained environments");
System.out.println("• Consider: Build time, runtime performance, memory usage, and startup time requirements");
}
}

Key Comparison Summary

AspectJVM ModeNative Mode
Startup Time1-3 seconds10-50 milliseconds
Memory Usage100-500 MB20-100 MB
Build Time30-60 seconds2-5 minutes
Executable Size10-50 MB + JVM50-100 MB
Peak ThroughputSlightly higherSlightly lower
Development ExperienceExcellentGood (requires rebuild)
DebuggingFull debugging supportLimited debugging
ReflectionFull supportRequires configuration
Dynamic Class LoadingFully supportedLimited support

When to Choose Each Mode

Choose JVM Mode When:

  • Development phase with frequent code changes
  • Complex applications with dynamic class loading
  • Applications requiring extensive reflection
  • CI/CD pipelines where build time matters
  • Applications with complex classpath requirements

Choose Native Mode When:

  • Microservices and serverless deployments
  • Resource-constrained environments
  • Fast startup time is critical (FaaS, containers)
  • Reduced memory footprint is important
  • Production deployments with stable code

This comprehensive comparison helps developers make informed decisions about when to use Quarkus in JVM mode versus native mode based on their specific requirements and constraints.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper