What do the native-image optimization levels (-O0 through -O3, and -Ob) mean, and which would you use for a production throughput-bound Spring service versus local development?
answer
- -O0 debug, -O2 default, -O3 aggressive
- -Ob = build time, not runtime
- dev -> -Ob, prod -> -O3 + PGO
- -O3 is Oracle GraalVM
- higher O = slower build
basics
~20 s-O sets compiler optimization: -O0 none (debugging), -O2 the production default, -O3 more aggressive (Oracle GraalVM), -O1 in between. -Ob optimizes build time, not runtime — use it for fast local dev builds, never for production throughput.
solid answer
~40 sThe -O build arg controls how hard native-image optimizes the generated code. -O0 disables optimizations and keeps debug info, for debugging only. -O1 is a light level. -O2 is the default and the right baseline for production. -O3 is a more aggressive level available in Oracle GraalVM and pairs well with PGO for throughput-bound services. Separately, -Ob is the 'optimize for build time' mode: it makes the build faster and cheaper at the cost of runtime performance, which is ideal for the inner dev loop where you rebuild constantly but wrong for production. So: local dev iteration uses -Ob (or -O0 when debugging); a throughput-bound production service uses -O2 as the floor and -O3 (with PGO) when you need to squeeze out peak performance. You pass it through the Native Build Tools buildArgs.
code
kotlin · 17 lines// build.gradle.kts — pick the optimization level per build profile
import org.graalvm.buildtools.gradle.dsl.GraalVMExtension
configure<GraalVMExtension> {
binaries {
named("main") {
val level = when (project.findProperty("buildProfile")) {
"dev" -> "-Ob" // fastest builds for the inner loop
"debug" -> "-O0" // no opt, keep debug info
else -> "-O3" // release: max runtime perf (Oracle GraalVM)
}
buildArgs.add(level)
}
}
}
// Dev: ./gradlew nativeCompile -PbuildProfile=dev
// Release: ./gradlew nativeCompile (defaults to -O3)go deeper
Know -O2 is the default and -Ob is about build time, not runtime speed.
List the levels and pick the right one per environment (dev -Ob, prod -O2/-O3).
Tie -O3 to Oracle GraalVM + PGO and to CI build-time cost tradeoffs.
Set org-wide build profiles so dev iterates fast (-Ob) while release maximizes throughput (-O3+PGO), balancing CI cost.
## The -O flag `-O<n>` is a `native-image` build argument that trades **build effort/time** against **generated-code quality (runtime performance)**. | Level | Meaning | Use it for | |-------|---------|-----------| | `-O0` | No optimizations, preserves debug info | Debugging the native image (with `-g`) | | `-O1` | Basic optimizations | Rarely chosen explicitly | | `-O2` | **Default**, balanced production optimization | Production baseline | | `-O3` | More aggressive optimization (**Oracle GraalVM**) | Throughput-bound prod, especially with PGO | | `-Ob` | Optimize **build time** (faster, cheaper builds; lower runtime perf) | Inner dev loop / fast CI feedback | **Key distinction:** `-O0..-O3` optimize the *runtime* of the produced binary (higher number = more runtime optimization, slower build). `-Ob` is different — the **'b' is for build**: it deliberately reduces optimization to make the **build itself faster**, so it *lowers* runtime performance. Confusing `-Ob` with a high runtime optimization is a classic trap. ## Choosing per environment - **Local development / fast iteration:** `-Ob` — you rebuild constantly and don't care about peak throughput; the faster build wins. Use `-O0 -g` when you specifically need to debug. - **Production, throughput-bound:** `-O2` is the floor. Move to `-O3` (Oracle GraalVM) when you need maximum steady-state performance, and combine it with **PGO** — `-O3` plus a good profile is where the biggest native throughput wins come from. - **Startup/footprint-only services:** `-O2` default is plenty; `-O3` mainly buys throughput, not startup. ## Wiring into Spring Boot Pass through the GraalVM Native Build Tools plugin: Gradle `graalvmNative { binaries { named("main") { buildArgs.add("-O3") } } }`, or Maven `native-maven-plugin` `<buildArgs><buildArg>-O3</buildArg></buildArgs>`. Teams often gate the level on a build profile so dev uses `-Ob` and the release pipeline uses `-O3`. ## Gotchas - `-O3` is an **Oracle GraalVM** level; on Community Edition stick to `-O2`. - Higher `-O` means **longer, more expensive builds** — native builds are already the slow part of CI. - `-Ob` in production silently costs you throughput; it's the most common misconfiguration. - `-O` is orthogonal to the GC choice and to PGO; you typically combine all three.
- A teammate set -Ob for production 'to optimize'. What's wrong?-Ob optimizes build time, not runtime — it reduces optimization to build faster, so it lowers production throughput. Production should be -O2 (floor) or -O3, and -Ob belongs in the dev loop.
- Where do -O levels give the most benefit?Combined with PGO on a throughput-bound service: -O3 plus a representative profile recovers most of the JIT gap. For startup-only workloads the default -O2 is already enough.
saying these in an interview costs you the question
- Reading -Ob as a higher runtime optimization than -O3
- Using -O0 or -Ob for production
- Assuming -O3 is available on Community Edition