skip to content

JVM Toolchains

Declaring the JDK a build compiles and tests with, independent of the JVM running Gradle: vendor selection, detection, auto-provisioning, and per-task overrides. Interviewers ask because toolchains end the whole JAVA_HOME-mismatch class of problems.

on this pageshow

explore

questions

page 1 of 2

How do you tell an Android Gradle Plugin (AGP) project which JDK to use to compile Java and Kotlin, using a JVM toolchain?

level: juniorimportance: must knowfreq 55%

answer

  1. jvmToolchain(17)
  2. decouples Gradle JDK from compile JDK
  3. java { toolchain { languageVersion } }
  4. AGP applies to JavaCompile
  5. reproducible builds

basics

~10 s

Set a JVM toolchain language version. With the Kotlin plugin use kotlin { jvmToolchain(17) }; AGP picks up that JDK to compile Java and Kotlin instead of the JDK running Gradle.

solid answer

~30 s

A JVM toolchain decouples the JDK that *runs* Gradle from the JDK that *compiles* your code. In an AGP project the simplest declaration is `kotlin { jvmToolchain(17) }` (Kotlin Gradle Plugin), which Gradle resolves to a real JDK 17 install (auto-provisioning it if a resolver is configured). The Kotlin plugin propagates that toolchain to Kotlin compilation, and AGP aligns Java compilation and `compileOptions` source/target to it. You can also configure `java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }` directly. The benefit: the build is reproducible and independent of whatever JDK the developer or CI machine happens to launch Gradle with.

code

kotlin · 11 lines
kotlin
// app/build.gradle.kts
kotlin {
    jvmToolchain(17)
}

// equivalent low-level form:
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

go deeper

for a junior

Know the one-liner kotlin { jvmToolchain(17) } and that it picks the JDK used to compile.

for a middle

Explain the Gradle-JDK vs toolchain-JDK distinction and that the Kotlin plugin propagates the toolchain to both Java and Kotlin.

for a senior

Discuss reproducibility, auto-provisioning fallback, and how AGP's JavaCompile inherits the toolchain language version.

for a principal

Standardize toolchain language versions across an Android monorepo so every module and CI agent compiles identically regardless of the launching JDK.

## What a JVM toolchain is Gradle separates two JDKs: - the **Gradle JDK** — the JVM that launches and runs the Gradle daemon; - the **toolchain JDK** — the JVM used to *compile* and (optionally) test your code. A **JVM toolchain** lets you declare the second one by *language version* (`17`, `21`, …) rather than hard-coding a path. Gradle then locates a matching JDK on the machine (or downloads one if auto-provisioning is enabled). This makes the build reproducible regardless of which JDK started Gradle. ## Declaring it in an AGP build There are two common entry points: 1. **Via the Kotlin Gradle Plugin** (recommended in Android, since most modules are Kotlin): `kotlin { jvmToolchain(17) }`. This is sugar over the Java toolchain — it sets the same `JavaLanguageVersion` and wires it into Kotlin compilation. 2. **Via the `java` extension directly:** `java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }`. ## How AGP consumes it AGP reads the project's Java toolchain and applies its language version to `JavaCompile` tasks. When the Kotlin plugin and AGP agree on the same toolchain, both Java and Kotlin compile against JDK 17, and `compileOptions.sourceCompatibility/targetCompatibility` default to the toolchain language version unless overridden. ## Why prefer a toolchain over `JAVA_HOME` - Reproducible: CI and laptops compile with the same JDK. - The Gradle daemon can keep running on a newer JDK (e.g. 21) while still producing JDK-17 bytecode. - It plays with Foojay auto-provisioning so missing JDKs download automatically. ```kotlin // app/build.gradle.kts plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { compileSdk = 34 // compileOptions left to inherit the toolchain language version } kotlin { jvmToolchain(17) // one line: Java + Kotlin compile on JDK 17 } ```

  • Does `jvmToolchain(17)` configure both Java and Kotlin compilation?
    Yes. The Kotlin plugin sets the Java toolchain language version and wires Kotlin compilation to the same JDK, so both `JavaCompile` and Kotlin compile tasks run on JDK 17.
  • What happens if no JDK 17 is installed locally?
    Gradle fails to resolve the toolchain unless auto-provisioning (e.g. the Foojay resolver plugin) is configured, in which case it downloads a matching JDK.

The Gradle JDK is the kitchen you cook in; the toolchain JDK is the recipe's required oven temperature — you fix the temperature so the dish comes out the same no matter whose kitchen runs it.

saying these in an interview costs you the question

  • Claiming the toolchain changes which JDK runs the Gradle daemon — it only changes the compile JDK.
  • Confusing toolchain *language version* with `compileSdk`/`targetSdk` (Android API levels).

context

open as a page

What is a Java toolchain in Gradle, and why would you declare one instead of just relying on whatever JDK launched Gradle?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A toolchain tells Gradle which exact JDK version to use to compile, test, and run your code. Declaring one decouples that from the JDK that started Gradle, so the build uses the JDK you specify, not whatever happens to be on PATH.

open as a page

What does Gradle's local JDK auto-detection do, and where does it look for installed JDKs?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Gradle scans well-known locations for installed JDKs so a declared toolchain can be matched to a local JDK without you configuring paths. It checks SDKMAN, asdf, Jabba, Homebrew, and standard OS install directories.

open as a page

Show the minimal settings.gradle.kts setup that lets Gradle download a missing JDK, and explain what each piece does.

level: juniorimportance: must knowfreq 40%

basics

~10 s

Apply the foojay-resolver-convention plugin in settings.gradle.kts. It registers the Foojay repository under toolchainManagement, so Gradle can auto-download a missing toolchain JDK.

open as a page

What is JDK auto-provisioning in Gradle, and what makes it work?

level: juniorimportance: must knowfreq 55%

basics

~10 s

If a build needs a JDK that isn't installed locally, Gradle can automatically download it. By default Gradle needs a toolchain resolver plugin (foojay-resolver) registered in settings to know where to fetch JDKs from.

open as a page

How do you constrain a Gradle JVM toolchain to a specific JDK vendor, and what does adding a vendor do beyond just setting the language version?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Inside the toolchain block you set languageVersion and add vendor = JvmVendorSpec.ADOPTIUM. The vendor narrows detection/provisioning so only that distribution's JDK matches, not just any JDK of the right version.

open as a page

How do AGP's `compileOptions.sourceCompatibility`/`targetCompatibility` relate to a JVM toolchain, and which one should you set?

level: middleimportance: must knowfreq 50%

basics

~10 s

The toolchain selects which JDK compiles; compileOptions sets the Java bytecode level (source/target). With a toolchain set, source/target default to the toolchain version, so you usually only need the toolchain.

open as a page

Explain the difference between the 'Gradle JVM' and the 'build JDK' when a toolchain is declared. Can a build run Gradle on JDK 17 but compile with JDK 21?

level: middleimportance: must knowfreq 60%

basics

~20 s

The Gradle JVM is the JDK that launches Gradle; the build JDK is the toolchain JDK that compiles and runs your code. They can differ — yes, Gradle can run on JDK 17 while a declared toolchain compiles with JDK 21.

open as a page

How do you tell Gradle about JDKs installed in non-standard locations using org.gradle.java.installations.paths and fromEnv?

level: middleimportance: must knowfreq 45%

basics

~10 s

Set org.gradle.java.installations.paths to a comma-separated list of JDK home directories, or org.gradle.java.installations.fromEnv to a comma-separated list of env-var names whose values are JDK homes. Gradle probes those and adds them to detection.

open as a page

What is the toolchainManagement block in settings.gradle.kts, and why does Gradle require it for JDK auto-provisioning?

level: middleimportance: must knowfreq 45%

basics

~10 s

It's a settings.gradle.kts block where you register the resolver plugins (repositories) Gradle is allowed to download JDKs from when a toolchain is missing locally.

open as a page

What does it mean to set a per-task toolchain in Gradle, and how do you make a single Test task run on a different JDK than the rest of the build?

level: middleimportance: must knowfreq 55%

basics

~10 s

Use the javaToolchains service to build a launcher for a specific JDK version, then assign it to the task's javaLauncher property: test.javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }.

open as a page

How do you disable JDK auto-download, and why might a team do that in CI?

level: middleimportance: must knowfreq 48%

basics

~10 s

Set org.gradle.java.installations.auto-download=false in gradle.properties (or pass it with -P/-D). Gradle then refuses to download missing JDKs and fails if no matching local JDK exists. Teams do this so CI uses only pre-installed, controlled JDKs.

open as a page

What does the Kotlin Gradle Plugin's `jvmToolchain()` actually do under the hood in an Android module, and how does it differ from the raw `java.toolchain` block?

level: middleimportance: should knowfreq 35%

basics

~20 s

jvmToolchain(17) is a Kotlin-plugin convenience that sets the Java toolchain language version and wires Kotlin compile tasks to the same JDK. The raw java.toolchain block only configures Java; Kotlin needs the plugin to follow it.

open as a page

An AGP module has no toolchain declared and no compileOptions set. What JDK compiles the code and what bytecode is emitted — and why is that a problem?

level: middleimportance: should knowfreq 28%

basics

~10 s

Without a toolchain, AGP compiles using the JDK that runs Gradle (the daemon JDK), and defaults to a low source/target (historically Java 8). That's a problem because output depends on whoever's machine launched Gradle.

open as a page

When you declare a toolchain with languageVersion 21, what bytecode level does compilation target, and how does this interact with sourceCompatibility/targetCompatibility and the release flag?

level: middleimportance: should knowfreq 45%

basics

~20 s

By default a JDK-21 toolchain compiles to Java 21 bytecode. If you still want older bytecode, set sourceCompatibility/targetCompatibility (or release) lower than the toolchain version. The toolchain picks the JDK; source/target/release set the bytecode level.

open as a page

A teammate's build fails with 'No compatible toolchains found' even though they have the right JDK installed. How do you debug the detection?

level: middleimportance: should knowfreq 40%

basics

~10 s

Run ./gradlew javaToolchains to see what Gradle actually discovered. If the JDK isn't listed, it's in a non-scanned location or detection is off — add it via installations.paths/fromEnv or re-enable auto-detect.

open as a page

If you register no toolchain repositories, or want to forbid downloads entirely, how does Gradle behave and how do you control auto-download independently of toolchainManagement?

level: middleimportance: should knowfreq 25%

basics

~10 s

With no repository registered, Gradle can't download a missing JDK and fails if it can't find one locally. You can also globally disable downloads with the property org.gradle.java.installations.auto-download=false.

open as a page

How do you make a custom JavaExec task (or the application run task) execute on a specific JDK without changing the project-wide toolchain?

level: middleimportance: should knowfreq 35%

basics

~10 s

Set the JavaExec task's javaLauncher to a launcher from javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(X) }. The project's java.toolchain stays unchanged.

open as a page

What's the difference between the foojay-resolver and foojay-resolver-convention plugin IDs, and which do you apply where?

level: middleimportance: should knowfreq 40%

basics

~10 s

Both come from the same project. foojay-resolver only registers the resolver, so you must wire toolchainManagement yourself. foojay-resolver-convention registers the resolver AND wires up the default javaRepositories for you. You apply either in settings.gradle.kts.

open as a page

What does `implementation = JvmImplementation.J9` do in a toolchain spec, and how does it combine with the vendor constraint?

level: middleimportance: should knowfreq 30%

basics

~10 s

implementation selects the JVM engine. The default is VENDOR_SPECIFIC (the vendor's normal HotSpot-based JDK); J9 restricts to OpenJ9-based builds (e.g. IBM Semeru). It's an extra filter on top of vendor and version.

open as a page

When would you use `JvmVendorSpec.matching("...")` instead of a named vendor constant, and what are the trade-offs?

level: middleimportance: should knowfreq 22%

basics

~10 s

JvmVendorSpec.matching("string") matches any JDK whose vendor metadata contains that substring. Use it for a distribution with no named constant. The trade-off: it's free-text, so a too-loose string can match unintended JDKs.

open as a page

You run CI on a JDK 21 agent but ship Android bytecode that must build identically everywhere. How do toolchains let the Gradle daemon stay on JDK 21 while AGP compiles on a fixed JDK?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Declare a fixed toolchain (e.g. jvmToolchain(17)). The daemon keeps running on JDK 21, but Gradle resolves JDK 17 for AGP's JavaCompile and Kotlin compile, so output is identical regardless of the launching JDK.

open as a page

The toolchain languageVersion is a lazy Property. Why does Gradle model it lazily rather than resolving the JDK at configuration time?

level: seniorimportance: should knowfreq 30%

basics

~20 s

languageVersion is a Property<JavaLanguageVersion>, so the requested version is recorded lazily and the actual JDK is resolved only when a task that needs it runs. This keeps configuration fast, lets values come from providers, and stays configuration-cache compatible.

open as a page

A legacy build uses sourceCompatibility/targetCompatibility and org.gradle.java.home to pin its JDK. How would you migrate it to a declared toolchain, and what behaviour changes should you expect?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Replace org.gradle.java.home + sourceCompatibility/targetCompatibility with a java { toolchain { languageVersion = ... } } block. Now Gradle, not the developer's environment, selects the compile JDK, and the Gradle daemon can run on a different JDK.

open as a page

Why and how would you disable auto-detection with org.gradle.java.installations.auto-detect=false?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Set org.gradle.java.installations.auto-detect=false to stop Gradle scanning managers and OS dirs. You then supply JDKs only via installations.paths/fromEnv. It's used on locked-down CI to make the JDK set deterministic and the build fast.

open as a page

What is the difference between the foojay-resolver-convention plugin and manually writing a javaRepositories block, and when would you choose the manual form?

level: seniorimportance: should knowfreq 30%

basics

~10 s

The convention plugin auto-registers the Foojay repository for you. Writing javaRepositories manually lets you control resolver ordering, register multiple resolvers, or use a custom resolver.

open as a page

Inside a repository(name) { } entry, what is resolverClass, and what does the bound type have to implement for provisioning to work?

level: seniorimportance: should knowfreq 18%

basics

~10 s

resolverClass binds a repository name to a JavaToolchainResolver implementation. That class takes a toolchain request (version, vendor, platform) and returns an optional download URI Gradle uses to fetch the JDK.

open as a page

Walk me through the difference between a JavaCompile task's javaCompiler and a Test/JavaExec task's javaLauncher. When would you set each to different JDKs in the same build?

level: seniorimportance: should knowfreq 40%

basics

~20 s

javaCompiler picks the JDK that compiles sources; javaLauncher picks the JDK that runs a process (tests or JavaExec). You set them differently to, e.g., compile bytecode for Java 17 but run tests on Java 21.

open as a page

Why do javaToolchains.launcherFor/compilerFor return Providers, and what practical consequences does that laziness have for a per-task toolchain override?

level: seniorimportance: should knowfreq 30%

basics

~20 s

They return Providers so the JDK is located only when the task runs, not at configuration time. So referencing a JDK you don't have is free unless the task actually executes, and assignment integrates with Gradle's lazy configuration and up-to-date checks.

open as a page

When a toolchain is requested, how does Gradle decide between a local JDK and provisioning a new one?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Gradle always prefers a matching locally installed JDK. It scans known locations first; only if nothing matches the requested version/vendor/implementation does it provision via a resolver — and only if auto-download isn't disabled.

open as a page

showing 1–30 of 35