skip to content

Your KMP service must run on a production JVM 17 fleet but you want to build with the latest JDK. How do you configure the jvm() target, and what risks arise from getting the bytecode level wrong?

level: seniorimportance: should knowfreq 35%

answer

  1. jvmTarget = JVM_17 -> class major 61 -> loads on JVM 17
  2. toolchain 21 to build, valid since 21 >= 17
  3. too-new bytecode -> UnsupportedClassVersionError at load
  4. newer java.* API -> NoSuchMethodError at runtime
  5. use Java --release 17 + align Kotlin/Java to avoid mismatch

basics

~10 s

Pin jvmTarget to JVM_17 so the bytecode loads on JVM 17, while using a newer JDK toolchain to compile. If bytecode is too new, production throws UnsupportedClassVersionError at load time.

solid answer

~40 s

Set `compilerOptions.jvmTarget = JvmTarget.JVM_17` so every emitted class file has the version 17 (major 61) marker, guaranteeing it loads on the JVM 17 fleet. Use `jvmToolchain(21)` (or your latest) so developers compile and test with a modern JDK — valid because the toolchain version is >= the target. Critically, also keep your **API usage** within JVM 17: a higher toolchain still lets you call newer `java.*` methods that don't exist on 17, which compiles fine but fails at runtime with `NoSuchMethodError`. If you mistakenly emit higher-than-17 bytecode, production JVMs reject it at class load with **`UnsupportedClassVersionError`**. In mixed Kotlin/Java modules, align Java's `release` (or `--release 17`) with Kotlin's jvmTarget; the `--release` flag is stronger than just `-target` because it also blocks newer-API usage. Set both consistently to avoid the `Inconsistent JVM-target compatibility` error.

code

kotlin · 11 lines
kotlin
kotlin {
    jvmToolchain(21)
    jvm {
        compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
    }
}

// Mixed-module Java side (align the floor + restrict API surface):
tasks.withType<JavaCompile> {
    options.release.set(17)   // compiles against JDK 17 API signatures
}

go deeper

for a junior

Knows you set jvmTarget to the runtime version so it can run.

for a middle

Pins jvmTarget to 17 and uses a newer toolchain, knowing target <= toolchain.

for a senior

Explains the API-floor vs bytecode-floor distinction, UnsupportedClassVersionError vs NoSuchMethodError, and aligns Java --release.

for a principal

Establishes org policy for build-JDK vs deploy-floor, CI on production JVMs, and migration plans when bumping the fleet.

## The scenario Production runs **JVM 17**; you want the ergonomics of the newest JDK for compiling and testing. The JVM target gives you exactly the two knobs to do this safely. ## Configuration ```kotlin kotlin { jvmToolchain(21) // build & test with JDK 21 jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) // emit JVM 17 bytecode } } } ``` - **`jvmTarget = JVM_17`** stamps each `.class` with **class-file major version 61** (= Java 17), so a JVM 17 can load it. Valid because the toolchain (21) >= target (17). - **`jvmToolchain(21)`** selects (auto-provisioning if needed) JDK 21 to run `javac`/`kotlinc` and tests. ## The subtle trap: API floor vs bytecode floor Setting `jvmTarget` controls **bytecode version**, not **which library methods exist**. With a JDK 21 toolchain you can accidentally call a method that only exists in newer `java.*` (e.g., a method added in JDK 19). It compiles, the bytecode is version 17, it loads on JVM 17 — then throws **`NoSuchMethodError`** / `NoSuchFieldError` at runtime when that method is invoked. Defenses: - For the Java parts, use **`--release 17`** (Java's `options.release`), which compiles against the **JDK 17 API signatures**, rejecting newer-API usage at compile time. Kotlin's `jvmTarget` does **not** have an equivalent API-restriction guarantee, so be deliberate about which JDK APIs you call, or run integration tests on JVM 17. - Run at least smoke/integration tests on a JVM matching production. ## Failure modes when the level is wrong - **Bytecode too new** (e.g., emitted 21 bytecode reaches a JVM 17): class load fails with **`UnsupportedClassVersionError: has been compiled by a more recent version of the Java Runtime`**. This happens at load time, not compile time. - **Mismatched Kotlin vs Java targets** in the same module: Gradle/Kotlin reports **`Inconsistent JVM-target compatibility`**. Fix by setting Kotlin `jvmTarget` and Java `release`/`sourceCompatibility`/`targetCompatibility` to the same level. - **Newer-API usage on older runtime**: `NoSuchMethodError` at runtime (see above). ## Practical checklist - `jvmTarget` = production floor (here `JVM_17`). - Toolchain >= target (`21`). - Java `release` = same floor for mixed modules. - Verify on a production-matching JVM in CI. This separation lets one codebase target an older runtime while developers enjoy a newer build JDK.

  • Why doesn't setting jvmTarget=JVM_17 protect you from calling a JDK 19-only method?
    jvmTarget only fixes the bytecode version, not the API surface. The newer toolchain's classpath still exposes the method, so it compiles but throws NoSuchMethodError on JVM 17 at runtime.
  • What error does production show if it loads bytecode compiled to a newer level than the runtime supports?
    UnsupportedClassVersionError, raised by the class loader, citing that the class was compiled by a more recent Java runtime.

saying these in an interview costs you the question

  • Assuming jvmTarget also restricts which java.* APIs you can call
  • Thinking UnsupportedClassVersionError is a compile-time error
  • Forgetting to align Java release with Kotlin jvmTarget in mixed modules
  • Building and shipping without ever testing on a production-version JVM
  • Believing a newer toolchain alone forces newer bytecode

context