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