It is easy to configure native image creations in Gradle:
bootBuildImage {
environment = [
"BP_NATIVE_IMAGE": "true",
]
builder = "paketobuildpacks/builder-jammy-buildpackless-tiny"
buildpacks = [
"paketobuildpacks/java-native-image"
]
}
But with explicit settings of environment build arguments, upx compression, another run image it is possible to get significant better results regarding build time, image size, startup time and memory consumptions during runtime.
bootBuildImage {
environment = [
"BP_JVM_VERSION": "25",
"BP_NATIVE_IMAGE": "true",
"BP_NATIVE_IMAGE_BUILD_ARGUMENTS": [
// (default) mostly-static linked executable
"-H:+UnlockExperimentalVMOptions -H:+StaticExecutableWithDynamicLibC",
// no fallback if native-image generation fails
"--no-fallback",
// optimize for host machine
"-march=native",
// Parallel GC: Garbage-First (G1) GC based on Java HotSpot VM
"--gc=parallel",
// min/max heap size for binary, tested by locally with VM options -Xms -Xmx
"-R:MinHeapSize=32m -R:MaxHeapSize=48m",
// -Os: optimizations except those that can increase code or image size significantly
"-Os"
].join(" "),
"BP_BINARY_COMPRESSION_METHOD": "upx",
"BP_RUNTIME_CERT_BINDING_DISABLED": "true"
]
builder = "paketobuildpacks/builder-jammy-buildpackless-tiny"
buildpacks = [
"paketobuildpacks/java-native-image"
]
runImage = "busybox:1.36.1-glibc"
buildCache {
bind {
source.set("/tmp/paketobuildpacks/cache-${rootProject.name}.build")
}
}
launchCache {
bind {
source.set("/tmp/paketobuildpacks/cache-${rootProject.name}.launch")
}
}
}
bootBuildImage native-image | + upx compression + busybox runtime + defaults |
+ upx compression + busybox runtime + mem optimizations + default optimizations + parallel gc |
---|---|---|
Build Environment | BP_NATIVE_IMAGE BP_BINARY_COMPRESSION_METHOD:upx -O2 default (good performance at a reasonable file size) |
BP_NATIVE_IMAGE BP_BINARY_COMPRESSION_METHOD:upx -R:MaxHeapSize=48m -O2 default (good performance at a reasonable file size) -gc=parallel |
Build Time | 11m 27s | 11m 9s |
Build Image Size Based on "busybox:stable-glibc" |
39.2 MB | 39.4 MB |
Container Memory Usage | 274 MB (docker run w/o limit) 125 MB (docker run --memory=120m --memory-swap=0) |
199 MB (docker run w/o limit) 90 MB (docker run --memory=116m --memory-swap=0) |
CPU Usage Idle/Load | 0% / 2% | 0.2 % / mostly under 1%, seldom peaks to 18 % |
Startup Time | >= 1.3s | >= 1.1s |
Endpoint Response Times (GET localhost:8080) |
4ms to 60ms (mostly around 5 to 20ms) | mostly 1ms - 9 ms, sometimes > 10 ms, seldom peaks over 100ms |
Rating | ★★★☆☆ (missing memory optimizations) |
★★★★☆ (good startup and response times, low memory) |
The other GraalVM optimization levels -Ob and -Os were not considered:
- The level -Ob produced a binary which was worse than all other variants.
- The level -Os is not available in GraalVM 21, whereas it is documented in the reference manual for GraalVM 25.
bootBuildImage native-image | + upx compression + busybox runtime + mem optimizations + default optimizations + parallel gc |
+ upx compression + busybox runtime + mem optimizations + optimize for size + parallel gc |
---|---|---|
Build Environment | BP_RUNTIME_CERT_BINDING_DISABLED BP_NATIVE_IMAGE BP_BINARY_COMPRESSION_METHOD:upx -R:MaxHeapSize=48m -O2 default (good performance at a reasonable file size) -gc=parallel |
BP_RUNTIME_CERT_BINDING_DISABLED BP_NATIVE_IMAGE BP_BINARY_COMPRESSION_METHOD:upx -R:MaxHeapSize=48m -Os (optimizations, but mainly for image size) -gc=parallel |
Build Time | 17m 7s | 16m 28s |
Build Image Size Based on "busybox:stable-glibc" |
33.4 MB (without certificate bindings the image size is significantly smaller) |
29.2 MB (even smaller because of -Os) |
Container Memory Usage | 144 MB (docker run w/o limit) 61 - 69 MB (docker run --memory=90m --memory-swap=0) |
124 MB (docker run w/o limit) 66 MB (docker run --memory=90m --memory-swap=0) |
CPU Usage Idle/Load | 0.2 % / mostly under 1%, seldom peaks to 18 % | 0.2 % / mostly under 1%, seldom peaks to 18 % |
Startup Time | >= 1.0s | >= 1.0s |
Endpoint Response Times (GET localhost:8080) |
mostly 1ms - 9 ms, sometimes > 10 ms, seldom peaks over 100ms | mostly 1ms - 9 ms, sometimes > 10 ms, seldom peaks over 100ms |
Rating | ★★★★★ (good startup and response times, low memory) |
★★★★★ (good startup and response times, very low memory) |
For GraalVM 25 all documented optimization levels are available. Here the option -Os was used:
- The level -Os produces an even smaller image size. At least with this application no negative performance impacts were seen.
Common build image size optimizations
- Paketo's Java Native Image Buildpack uses mostly-static images as default, so a distro-less base image having glibc is sufficient as runtime OS. Each build has been done with "busybox:stable-glibc" as runtime image to optimize the build image size.
- Furthermore each native image has been compressed during build by the Ultimate Packer for eXecutables.
Kernel: Linux 6.8.0-83-generic
CPU: Intel(R) Core(TM) i3-4160T (4) @ 3.10 GHz
GPU: Intel 4th Generation Core Processor Family Integrated Graphics Controller @ 1.15 GHz [Integrated]
Memory: 12.49 GiB / 15.49 GiB (81%)
Swap: 4.55 GiB / 16.76 GiB (27%)
Build the native image:
./gradlew clean bootBuildImage
Run the image with docker:
docker run --rm --memory=116m --memory-swap=0 --name "spring-boot-native-image" -p 8080:8080 spring-boot-native-image:latest
- GraalVM Manual
- GraalVM - Native Image Build Output
- SBOM (Software Bill Of Materials) provided in Native Images
- GraalVM Community Edition Container Images
- Medium - GraalVM News
- Paketo Buildpacks Java How To
- Paketo Buildpacks for GraalVM
- Paketo Buildpack for Native Image
- Buildpacks IO - Pack CLI Tool