You install the requested JDK, reopen the project, and the build still insists it is running Java 8 or Java 11. That loop is maddening because the error sounds like an installation problem. Usually it is a selection problem: the JDK on your machine and the JVM that launched Gradle are not necessarily the same thing.

The durable fix is to identify the Android Gradle Plugin (AGP), the Gradle Wrapper, and the JVM running Gradle as three separate versions. Then change the one that is genuinely incompatible—not every Java-looking setting in the project.

First, let the failing build identify itself

Terminalbash
./gradlew --version
grep -n 'distributionUrl' gradle/wrapper/gradle-wrapper.properties
grep -RIn --include='*.gradle' --include='*.gradle.kts' --include='libs.versions.toml' 'com.android.tools.build:gradle\|com.android.application\|com.android.library' .

Three lines of evidence end a great deal of guesswork

  • ./gradlew --version starts the project Wrapper and reports both the Gradle version and the Launcher JVM/Daemon JVM. That JVM is the relevant evidence for a runtime-requirement error.

  • gradle-wrapper.properties selects the Gradle distribution. It does not select AGP or a JDK.

  • AGP may be declared as a plugins version, a version-catalog alias, or the older com.android.tools.build:gradle buildscript dependency; inspect the declaration your project actually uses.

  • These are read-only diagnostic commands. The example is documentation-validated and intentionally has no fabricated output because every project reports different versions.

Match the three moving parts before editing anything

  • AGP 7.x: the AGP 7.0 release notes establish JDK 11 as the minimum runtime, while each AGP release also specifies a compatible Gradle range.

  • AGP 8.x: Android’s build documentation states that AGP 8.x requires JDK 17; the AGP 8.0 compatibility table pairs it with Gradle 8.0 or newer for that release.

  • AGP 9.x: do not extrapolate from the major number. Open the exact release notes; for example, the AGP 9.4 compatibility table lists JDK 17 and Gradle 9.6.0.

  • Gradle itself: its compatibility matrix limits which Java versions can run a given Gradle release. A JDK can satisfy AGP yet still be too new for an old Wrapper.

  • Application bytecode: Java toolchains, sourceCompatibility, targetCompatibility, Kotlin targets, compileSdk, and desugaring govern compilation and device compatibility. They do not choose the JVM that starts Gradle.

When the failure happens inside Android Studio

  1. Open File > Settings on Windows/Linux, or Android Studio > Settings on macOS.

  2. Go to Build, Execution, Deployment > Build Tools > Gradle.

  3. Choose a compatible Gradle JDK. Android recommends GRADLE_LOCAL_JAVA_HOME in most cases for current projects; a named local JDK or JAVA_HOME macro can also be appropriate for a team policy.

  4. Sync, then run the same build from Studio again.

  5. If the terminal behaves differently, do not keep toggling this menu—the terminal follows a different selection path.

Android Studio records this choice as gradleJvm in .idea/gradle.xml. Newer Android Studio versions can use Gradle Daemon JVM criteria for supported projects. Follow the IDE migration prompt when offered and commit only the project settings your team intentionally shares.

When the failure happens in a shell

Terminalbash
printf 'JAVA_HOME=%s\n' "${JAVA_HOME:-<unset>}"
command -v java
java -version
./gradlew --version

The last command is the one that settles the argument

  • When JAVA_HOME is set, a terminal Gradle launch normally uses it; otherwise the Wrapper falls back to java found on PATH.

  • command -v java identifies the executable selected by this shell, while java -version reports its runtime.

  • ./gradlew --version reports the JVM that actually launched Gradle, so trust it over assumptions based on an installed-JDK list.

  • After changing shell startup files, open a new terminal or export the value in the current session before retesting.

  • The commands are safe and read-only; no output is shown because they were not run inside the reader’s Android project.

A temporary shell fix that proves the diagnosis

Terminalbash
export JAVA_HOME=/absolute/path/to/a/compatible/jdk
export PATH="$JAVA_HOME/bin:$PATH"
./gradlew --version
./gradlew assembleDebug

Why proving it temporarily is kinder to the machine

  • Replace the placeholder with the JDK home directory, not the path to the java executable.

  • export affects this shell and its child processes; it does not silently rewrite system-wide Java selection.

  • The PATH assignment makes direct java calls agree with JAVA_HOME for this session.

  • Verify before building. If the Wrapper still reports the old JVM, another Gradle runtime selector or daemon criterion is in play.

  • assembleDebug configures and compiles the project, so it may download dependencies and write build outputs even though it does not install an app.

Use org.gradle.java.home only with open eyes

gradle.propertiesproperties
org.gradle.java.home=/absolute/path/to/a/compatible/jdk

Pin the JVM that runs Gradle when a path-based override is genuinely required.

A strong override has a portability cost

  • org.gradle.java.home selects the Java home for the Gradle build process; it does not configure Android device APIs.

  • A machine-specific absolute path in the project’s gradle.properties usually breaks another developer or CI runner. Prefer a user-level property, environment provisioning, the IDE’s project-aware macro, or supported Daemon JVM criteria.

  • Do not commit a path containing a personal username or a vendor-specific directory unless every build agent intentionally provides that exact path.

  • Gradle properties have precedence rules. If behavior is surprising, inspect project and user-level gradle.properties, command-line system properties, and IDE selection rather than adding a second override blindly.

Pin compilation separately with a Java toolchain

app/build.gradle.ktskotlin
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Ask Gradle to compile Java sources with a Java 17 toolchain.

This solves repeatability, not the startup paradox

  • The toolchain provides the compiler, Javadoc runtime, and unit-test defaults used for Java work.

  • Android recommends declaring a toolchain so developer and CI machines do not compile with accidental local defaults.

  • Gradle still needs a compatible JVM before it can evaluate this build script and locate the toolchain.

  • Toolchain 17 does not by itself grant Android 14 APIs to older devices; compileSdk, minSdk, and supported desugaring remain separate concerns.

Make CI reveal the same evidence

Terminalbash
java -version
./gradlew --version
./gradlew --no-daemon assembleDebug

A green laptop build is not a CI contract

  • Provision an explicit JDK version in the CI runner using the platform’s supported Java setup mechanism, then print the evidence before the build.

  • Commit gradlew, gradlew.bat, gradle-wrapper.jar, and gradle-wrapper.properties; the Wrapper keeps the Gradle version consistent across environments.

  • --no-daemon is useful for an isolated CI job but is not required for every runner; it prevents a persistent daemon from obscuring this diagnostic run.

  • Cache directories only after the Java and Wrapper contract is stable. A cache should accelerate a correct environment, not define it.

If it still reports the “wrong” Java

  • Studio succeeds, terminal fails: align Android Studio’s Gradle JDK and shell JAVA_HOME, then compare both ./gradlew --version results.

  • Terminal succeeds, Studio fails: change the Gradle JDK for this project; changing Studio’s own boot runtime is not the same fix.

  • AGP wants 17 but Java 17 breaks immediately: the Wrapper may be too old to run on 17. Check the AGP release table and Gradle Java compatibility matrix as a pair.

  • The version changed but the daemon looks stale: run ./gradlew --stop, then repeat ./gradlew --version. Gradle normally separates daemons by JVM and Gradle version, but a clean observation helps.

  • A teammate cannot use committed gradle.properties: remove the machine-specific JDK path and provision Java through a portable team mechanism.

  • The app must target old Android devices: do not downgrade the build JVM reflexively. Runtime JDK, compiler toolchain, bytecode target, Android API availability, and desugaring are different layers.

  • `gradle --version` and `./gradlew --version` disagree: that is expected when a globally installed Gradle differs from the project Wrapper. Build with the Wrapper.

The satisfying ending is consistency

A Java runtime error often arrives after an AGP upgrade, so it feels as though the project suddenly forgot how to build. It did not. One component raised its contract while one launch path kept an older JVM.

Once Studio, terminal, and CI all report a compatible Gradle JVM—and the Wrapper matches the selected AGP—the error stops being mysterious. Better still, the next engineer will see the same build you see.

Continue with the Android build system

Primary documentation behind this diagnosis