Supercharge Your Quarkus Containers: Auto-Tune JVM Memory with Microsoft JAZ
A hands-on guide for Java developers: Smarter heap sizing, zero manual tuning, and rock-solid stability inside containers.
Deploy smarter, not harder.
Modern Java workloads run inside tight container budgets.
Quarkus helps, but your JVM still needs sane memory and GC settings; otherwise, you risk OOM kills, unpredictable latency, and noisy debugging sessions.
Microsoft JAZ (the Azure Command Launcher for Java) solves this by auto-tuning heap, GC, and runtime parameters based on container limits. Even better: no installs required when you use the Microsoft Build of OpenJDK container images.
This tutorial shows you how to:
• Build a super simple Quarkus app
• Run it in Microsoft’s OpenJDK container image
• Let JAZ handle all JVM tuning automatically
• Validate the tuning via a live endpoint
Container orchestrators schedule your pods on nodes with different memory footprints. JVM defaults assume full host memory, not cgroup limits.
Without tuning:
• JVM silicates too little heap by default
• Container hits its memory quota
• Kubernetes OOMKills your pod
With JAZ:
• It detects the container memory limit
• Calculates heap sizing
• Applies tuned GC flags
• Ensures stability under tight budgets
Quarkus + JAZ gives you predictable performance without hand-tuned flags.
Prerequisites
• JDK 21
• Maven 3.8+
• Podman or Docker
Create the Quarkus Application
We create a simple /memory endpoint to inspect the JVM heap.
This proves that JAZ is overriding the JVM settings inside the container.
Bootstrap the project
Run below or check out the code from the repository.
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=jaz-quarkus-demo \
-Dextensions="rest-jackson,quarkus-container-image-podman"
cd jaz-quarkus-demoAdd a memory inspection resource
Rename GreetingResource to src/main/java/com/example/MemoryResource.java and replace with the following:
package com.example;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(”/memory”)
public class MemoryResource {
public record MemoryStats(
String status,
long jvmMaxHeapMB,
long jvmUsedHeapMB,
String hostContainerOS,
List<String> jvmArguments
) {}
@GET
@Produces(MediaType.APPLICATION_JSON)
public MemoryStats getMemoryStats() {
return gatherMemoryStats();
}
private MemoryStats gatherMemoryStats() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long heapMax = memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024);
long heapUsed = memoryBean.getHeapMemoryUsage().getUsed() / (1024 * 1024);
List<String> jvmArgs = ManagementFactory
.getRuntimeMXBean()
.getInputArguments();
Set<String> uniqueJvmArgs = new LinkedHashSet<>(jvmArgs);
List<String> jvmArgumentsList = uniqueJvmArgs.stream().toList();
return new MemoryStats(
“JAZ Status Report”,
heapMax,
heapUsed,
System.getProperty(”os.name”),
jvmArgumentsList
);
}
}Package the app
(Delete the scaffolded tests or grab the example from my Github repository. I did indeed rename and change the scaffolded ons there ;-))
mvn packageQuarkus produces target/quarkus-app/ which will be copied into the container.
Build a JAZ-powered Container Image
Quarkus scaffolds various container files for you. In order to change to the Microsoft OpenJDK base image, you need to adjust the existing and change the base image:
Adjust the Dockerfile.jvm in src/main/docker and make the following changes:
FROM mcr.microsoft.com/openjdk/jdk:21-azurelinux
ENV LANGUAGE=’en_US:en’
# Create deployments directory (matching Quarkus layout)
RUN mkdir -p /deployments
# Copy JARs generated by mvn package
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
# Run as non-root (185 = Quarkus default user)
USER 185
# Set Java options
ENV _JAVA_OPTIONS=”-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager”
# Use a normal java -jar entrypoint
ENTRYPOINT [ “jaz”, “-jar”, “/deployments/quarkus-run.jar” ]Let’s also tweak the image name settings a little in application.properties (docs):
quarkus.container-image.registry=
quarkus.container-image.group=
quarkus.container-image.name=jaz-quarkus-demo
quarkus.container-image.tag=latestBuild & Run with Memory Constraints
JAZ only activates when the container has limits.
Build the image
quarkus build -Dquarkus.container-image.build=trueConfirm JAZ exists
podman run --rm --entrypoint /bin/sh localhost/jaz-quarkus-demo -c 'command -v jaz'Expected:
/usr/sbin/jazRun with memory limits
Let’s constrain the container to 1 GB:
podman run -i --rm \
--name jaz-demo \
--memory=1g \
--cpus=1.0 \
-p 8080:8080 \Verify the Auto-Tuning
Open a new terminal:
curl http://localhost:8080/memoryExpected output:
{
“status”: “JAZ Status Report”,
“jvmMaxHeapMB”: 734,
“jvmUsedHeapMB”: 10,
“hostContainerOS”: “Linux”,
“jvmArguments”: [
“-XX:NativeMemoryTracking=summary”,
“-XX:+UseG1GC”,
“-Xmx734m”,
“-XX:MinHeapFreeRatio=10”,
“-XX:MaxHeapFreeRatio=50”,
“-XX:+UnlockExperimentalVMOptions”,
“-XX:+G1UseTimeBasedHeapSizing”,
“-XX:G1PeriodicGCInterval=10000”,
“-Dquarkus.http.host=0.0.0.0”,
“-Djava.util.logging.manager=org.jboss.logmanager.LogManager”
]
}Success.
JAZ detected the 1GB limit and tuned your JVM heap accordingly.
Why this is important
Without JAZ, the JVM might assume your host has 32GB RAM.
It might size the heap to 8GB.
But your container is capped at 1GB.
Result:
OOMKilled → CrashLoopBackOff → On-call team gets paged.
JAZ prevents this by:
• Reading cgroup limits
• Correctly sizing the heap
• Making GC choices based on real container limits
It removes the need to tune memory flags manually.
Try Dry-Run Mode
If you want to see which JVM flags JAZ will produce:
podman run -i --rm \
--name jaz-demo \
--memory=1g \
--cpus=1.0 \
-p 8080:8080 \
-e JAZ_DRY_RUN=1 \
localhost/jaz-quarkus-demo:latestOutput example:
jaz would run: [/usr/bin/java -XX:+UseG1GC -Xmx734m -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=50 -XX:+UnlockExperimentalVMOptions -XX:+G1UseTimeBasedHeapSizing -XX:G1PeriodicGCInterval=10000 -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /deployments/quarkus-run.jar]No application starts.
Perfect for audits or debugging.
Production Notes
Use this in real workloads when you have:
• Autoscaling pods with variable node sizes
• Strict JVM memory caps
• Multi-tenant clusters where “default heap size” is unsafe
• Highly optimized Quarkus apps where every MB matters
JAZ and Quarkus complement each other.
JAZ makes the JVM behave in containers.
Quarkus reduces how much JVM you need in the first place.
Where to go from here
• Try different container memory limits (256MB, 512MB, 2GB)
• Compare with running java -jar directly
• Use Quarkus Native to eliminate the JVM entirely
• Explore GC logs with JAZ’s verbose output
• Test autoscaling behavior with Horizontal Pod Autoscalers



