JAVA_HOME
The path to the root of a JDK installation. Build tools like Maven and Gradle and servers like Tomcat use it to pick which Java to run with — the java command itself never reads it; PATH decides that.
Last updated:
JAVA_HOME points at the root directory of a JDK installation — the folder that contains bin/, not bin/ itself. The java launcher never reads it: which Java runs when you type `java` is decided by PATH alone. What reads JAVA_HOME is the tooling layer around the JDK: Maven's mvn script refuses to start without a usable JDK and resolves the compiler through it, Gradle uses it to pick the JVM that runs the build (unless org.gradle.java.home or a toolchain overrides it), and Tomcat's catalina.sh locates the JVM through JAVA_HOME or JRE_HOME. That split is why `java -version` and your build tool can disagree about which JDK is in use.
- Provider
- General / OS
- Category
- runtime
- Set by
- Set manually in shell profiles, by SDKMAN!/jEnv, or by Windows JDK installers
- Example
- /usr/lib/jvm/java-21-openjdk-amd64
How to set JAVA_HOME
macOS (resolve the active JDK)
export JAVA_HOME=$(/usr/libexec/java_home -v 21)Linux (Debian/Ubuntu OpenJDK path)
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"Windows (PowerShell, current session)
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"verify what your build actually uses
mvn -version # shows the JAVA_HOME JDK
java -version # shows the PATH JDK — these can differWhy do java and Maven disagree about my Java version?
Because they resolve the JDK through two unrelated mechanisms. java on the command line is found via PATH — first matching binary wins. Maven's mvn launcher script explicitly uses $JAVA_HOME/bin/java when JAVA_HOME is set. Install a new JDK that updates one but not the other and you get the canonical confusion:
$ java -version # resolved via PATH
openjdk version "21.0.3"
$ mvn -version # resolved via JAVA_HOME
Apache Maven 3.9.6
Java version: 17.0.10, vendor: Eclipse AdoptiumThe fix is to derive both from the same root, in this order, in your shell profile:
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"What reads JAVA_HOME — and what doesn't
- Reads it: Maven (hard requirement — the launcher errors with "JAVA_HOME should point to a JDK, not a JRE" when misconfigured), Gradle (picks the JVM that runs the build unless
org.gradle.java.homeoverrides it), Tomcat'scatalina.sh(JRE_HOME takes precedence if both are set), Jenkins agents, Android tooling, and most IDE launcher scripts. - Ignores it: the
javaandjavacbinaries themselves. JAVA_HOME is a convention consumed by the tooling layer, never by the JVM launcher — which is exactly why the two can disagree.
Gradle has been quietly reducing JAVA_HOME's importance: toolchains (Gradle 6.7+) let the build declare "I need Java 17" and auto-provision a matching JDK, so the JVM compiling your code may be neither the PATH one nor the JAVA_HOME one. When debugging a Gradle build, trust gradle -version and the toolchain report over any environment variable.
Setting it per OS without hardcoding paths
# macOS — never hardcode /Library/Java/...; ask the resolver:
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
# Debian/Ubuntu — list candidates, then pin:
update-java-alternatives --list
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
# Anywhere — let a version manager own it:
sdk use java 21.0.3-tem # SDKMAN! exports JAVA_HOME for youOn Windows, JDK installers (Adoptium's MSI has an explicit "Set JAVA_HOME" checkbox) write it to the system environment. Two Windows-specific traps: don't include quotes in the value itself — scripts that expand %JAVA_HOME%\bin end up with quotes mid-path — and after editing System Properties, only newly opened terminals see the change.
JDK root, not bin — the off-by-one-directory bug
Every consumer of JAVA_HOME appends paths like /bin/java or /lib/tools.jar to it. Point it at .../jdk-21/bin and tools look for .../jdk-21/bin/bin/java; point it at the binary itself and they look for .../java/bin/java. The quick self-check is one line: "$JAVA_HOME/bin/java" -version — if that fails, every downstream tool will too. In Docker images the equivalent is checking the base image's convention first: the official eclipse-temurin images already set JAVA_HOME and PATH correctly, so re-setting them in your Dockerfile is usually copy-paste cargo culting. How ENV layering works there is covered in the Docker environment variables guide.
Reading config from Java itself
Inside the JVM, System.getenv("JAVA_HOME") returns whatever the process inherited — but the honest source of truth for "which Java am I running" is the system property System.getProperty("java.home"), which the JVM computes from its own install location and which cannot lie. The broader pattern of environment variables vs system properties in Java — including why -D flags and env vars get confused — is the subject of the Java environment variables guide.
References
Frequently Asked Questions
Do I need JAVA_HOME if java already works in my terminal?
For running java directly, no — PATH is enough. You need JAVA_HOME the moment Maven, Gradle, Tomcat, Jenkins, or an IDE launcher is involved, because those resolve the JDK through JAVA_HOME instead of PATH. Setting both, consistently, from the same JDK path avoids the two diverging.
Should JAVA_HOME include the bin directory?
No. Set it to the JDK root — the directory that contains bin/, lib/, and release. Tools build paths like $JAVA_HOME/bin/java themselves, so including /bin produces paths like .../bin/bin/java and fails.
Stay up to date
Get notified about new guides, tools, and cheatsheets.