Skip to content

Java 25 Performance Optimizations (AOT + Leyden + JVM Tuning) #101

@groldan

Description

@groldan

Java 25 Performance Optimizations (AOT + Leyden + JVM Tuning)

Description

Leverage Java 25 features and Spring Boot 3.5 AOT for significant performance improvements: 50-75% faster startup, ~30% memory reduction, sub-millisecond GC pauses.

Changes

1. Enable Spring Boot AOT Processing

File: src/artifacts/api/pom.xml

Add execution to spring-boot-maven-plugin:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>process-aot</id>
      <goals>
        <goal>process-aot</goal>
      </goals>
    </execution>
  </executions>
</plugin>

2. Generate Project Leyden AOT Cache

File: src/artifacts/api/Dockerfile

After the existing jarmode=layertools extract step, add an intermediate stage to generate the AOT cache:

# After layertools extraction (existing code):
# COPY ${JAR_FILE} application.jar
# RUN java -Djarmode=layertools -jar application.jar extract

# Add new stage for AOT cache generation:
FROM eclipse-temurin:25-jre as aot-cache
WORKDIR /opt/app/bin
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./

# Generate AOT cache with training run
RUN java -XX:AOTCacheOutput=/tmp/acl-service.aot \
     -Dspring.context.exit=onRefresh \
     org.springframework.boot.loader.launch.JarLauncher

# In final stage, copy the AOT cache:
COPY --from=aot-cache /tmp/acl-service.aot ./acl-service.aot

3. Update JVM Options

File: src/artifacts/api/Dockerfile

Replace the current JAVA_OPTS environment variable:

ENV JAVA_OPTS="-XX:MaxRAMPercentage=80 \
-XshowSettings:system \
-XX:AOTCache=/opt/app/bin/acl-service.aot \
-Dspring.aot.enabled=true \
-XX:+UseCompactObjectHeaders \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof \
-XX:+ExitOnOutOfMemoryError \
-XX:+UseContainerSupport \
-Djava.security.egd=file:/dev/./urandom"

Expected Benefits

Startup Time

  • 50-75% faster startup (Leyden AOT cache + Spring Boot AOT)
  • Benchmarks: Spring PetClinic 3.4s → 0.8s (4x improvement)
  • Multiplicative effect: 3x with Leyden alone, 4x when combined with Spring AOT

Memory Usage

  • ~30% reduction from Compact Object Headers
  • Reduced metaspace usage from AOT-generated code
  • Better CPU cache utilization

Garbage Collection

  • Sub-millisecond GC pauses with Generational ZGC
  • 10% throughput improvement over single-gen ZGC
  • No allocation stalls under load

JVM Warmup

  • Immediate optimization with method profiling data
  • JIT compiler uses pre-recorded profiles from training run
  • Eliminates typical "warmup" period

Technical Details

Java 25 Features Used

  • JEP 519: Compact Object Headers (production-ready)
  • JEP 514: AOT Command-Line Ergonomics (production-ready)
  • JEP 515: AOT Method Profiling (production-ready)
  • JEP 483: AOT Class Loading & Linking (from JDK 24)

How It Works

  1. Build-time: Spring Boot AOT analyzes application and generates optimized initialization code
  2. Docker build: Training run creates Leyden AOT cache with class loading and method profiling data
  3. Runtime: JVM loads pre-computed data from AOT cache, skips expensive initialization

AOT Cache Lifecycle

  • Generated during Docker image build (one-time per image)
  • Embedded in the Docker image
  • Automatically used by JVM when -XX:AOTCache flag is present
  • Must be regenerated when application code changes (automatic via Docker rebuild)

Implementation Notes

Prerequisites

  • Java 25 runtime (already in use via eclipse-temurin:25-jre)
  • Spring Boot 3.3+ (we're on 3.5.7)
  • Maven build process

Build Time Impact

  • AOT processing adds ~30-60 seconds to Maven build
  • Docker image build adds ~10-20 seconds for training run
  • Overall: acceptable for CI/CD pipelines

Image Size

  • AOT cache file: ~10-50 MB (depends on application size)
  • Spring AOT generated code increases JAR by ~10-20%
  • Trade-off: slightly larger image for significantly better runtime performance

Compatibility

  • All features are production-ready in Java 25
  • No experimental flags required
  • Works with existing Spring Boot 3.5 applications
  • Compatible with GeoTools (JVM AOT doesn't affect runtime reflection)

Testing Strategy

Phase 1: Baseline Metrics

Before changes, capture:

  • Application startup time
  • Memory usage (heap, RSS)
  • Response time percentiles (p50, p95, p99)
  • Throughput (requests/sec)

Phase 2: Deploy and Measure

After implementation:

  • Compare startup times (expect 50-75% improvement)
  • Monitor memory usage (expect ~30% reduction)
  • Verify GC pause times (expect <1ms)
  • Load test to ensure no regression in throughput

Phase 3: Production Rollout

  • Deploy to staging first
  • Monitor for 1-2 weeks
  • Gradual rollout to production
  • Keep previous image tagged for quick rollback

Monitoring

Key metrics to track:

jvm.memory.used
jvm.memory.committed
jvm.gc.pause (should be <1ms)
application.started.time (startup duration)
http.server.requests (response times)

Rollback Plan

If issues occur:

  1. Revert to previous Docker image (without AOT)
  2. Remove process-aot execution from pom.xml
  3. Remove AOT-related JVM flags from Dockerfile
  4. Rebuild and redeploy

No database migrations or code changes required for rollback.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions