Safeguard
DevSecOps

How to Change Java Versions on a Mac: jenv, SDKMAN, and JAVA_HOME

Three reliable ways to change Java version on a Mac — plain JAVA_HOME switching, jenv shims, and SDKMAN — with per-project pinning so builds stop depending on whatever your shell happens to export.

Marcus Chen
DevSecOps Engineer
6 min read

How to change Java version in Mac, in one line: point JAVA_HOME at a different installed JDK with export JAVA_HOME=$(/usr/libexec/java_home -v 17) — and if you switch often, let jenv or SDKMAN do that pointing for you automatically per project. Every Java version switcher on macOS is ultimately manipulating the same two things: the JAVA_HOME environment variable and which java binary wins in your PATH. Once you see that, the tools stop being magic and the failure modes ("Maven uses 21 but my terminal says 17") become debuggable. Here are the three approaches, from zero-dependency to fully managed, and how to pick.

First: know what you have installed

macOS ships a discovery tool that scans /Library/Java/JavaVirtualMachines:

/usr/libexec/java_home -V

Typical output lists each registered JDK with version, vendor, and path. Two things to check before any switching:

  • If a JDK you installed via Homebrew is missing here, its keg-only symlink was never created (see our brew Java install guide for the one-liner).
  • If you see ancient entries — a Java 8 from a 2019 installer, an EOL 11 — plan to delete them. Every stale JDK is an unpatched runtime that a misconfigured build can silently select. Cleaning the lineup is a small but real security chore.

Option 1: plain JAVA_HOME (no tools, fully transparent)

/usr/libexec/java_home -v <version> returns the path of the matching JDK, which makes a mac set java version function three lines of shell profile:

# ~/.zshrc
jdk() {
  export JAVA_HOME=$(/usr/libexec/java_home -v "$1")
  export PATH="$JAVA_HOME/bin:$PATH"
  java -version
}
$ jdk 17
openjdk version "17.0.x" ...
$ jdk 21
openjdk version "21.0.x" ...

Good for: people who switch occasionally and want zero moving parts. Limits: it is per-shell and manual — nothing pins a version to a project, and PATH grows a new prepend per switch (harmless, but inelegant). If you find yourself typing jdk 17 every time you cd into the same legacy service, graduate to a manager.

Option 2: jenv (shims + per-directory pinning)

jenv is the classic mac java version manager. It does not install JDKs — it manages which already-installed JDK is active, via shims on your PATH:

brew install jenv
# ~/.zshrc
export PATH="$HOME/.jenv/bin:$PATH"
eval "$(jenv init -)"

Register each installed JDK once:

jenv add $(/usr/libexec/java_home -v 17)
jenv add $(/usr/libexec/java_home -v 21)
jenv versions

Then set scope as needed:

jenv global 21          # machine default
jenv local 17           # this directory (writes .java-version)
jenv shell 21           # this shell session only

jenv local is the feature that pays rent: it drops a .java-version file in the project root, and from then on anyone with jenv who enters that directory gets the right JDK automatically. Commit the file; it is documentation that enforces itself.

Two flags worth enabling, because build tools do not read shims by default:

jenv enable-plugin export   # makes jenv also set JAVA_HOME
jenv enable-plugin maven    # keeps mvn on the jenv-selected JDK

The export plugin matters: without it, java obeys jenv while anything keyed off JAVA_HOME (Gradle daemons, IDE terminals, scripts) does not — the single most common jenv confusion.

Option 3: SDKMAN (installs and switches)

SDKMAN goes a step further than jenv: it downloads JDKs too, from a catalog of vendors (Temurin, Zulu, Corretto, GraalVM, Oracle), plus the rest of the JVM toolchain (Maven, Gradle, Kotlin):

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

A note on that install command, since this is a security blog: piping a remote script into bash is a trust decision. Fetch the script first, read it, then run it — thirty seconds of diligence that should be reflex for any tool installed this way.

sdk list java                     # browse available versions/vendors
sdk install java 21.0.4-tem      # identifiers = version-vendor, e.g. Temurin
sdk install java 17.0.12-tem
sdk use java 17.0.12-tem         # this shell
sdk default java 21.0.4-tem      # machine default

Per-project pinning uses .sdkmanrc:

sdk env init      # writes .sdkmanrc with the current version
sdk env           # activate it in this shell

Set sdkman_auto_env=true in ~/.sdkman/etc/config and the switch happens automatically on cd. (Exact available identifiers change monthly as vendors ship updates — always pick from a fresh sdk list java rather than copying versions from old blog posts, this one included.)

Good for: teams that want one tool to install, pin, and switch everything JVM. Limits: SDKMAN-installed JDKs live under ~/.sdkman and are not registered with /usr/libexec/java_home, so tools that rely on macOS-native discovery may not see them without extra configuration.

If you already use a polyglot manager like mise or asdf for Node and Python, both handle Java with the same .tool-versions pattern — a reasonable fourth option instead of adding a Java-specific tool.

Choosing, and making it stick for a team

  • Occasional switcher, JDKs from brew: plain java_home function.
  • Many projects, JDKs already installed: jenv with the export plugin.
  • Want installs + switching + Maven/Gradle management: SDKMAN.

Whichever you choose, the team-level win is committing the pin file (.java-version or .sdkmanrc) and enforcing the same version in CI — the local switcher stops drift on laptops, and the CI toolchain declaration stops "works on my machine." Gradle toolchains and Maven's enforcer plugin can hard-fail a build on the wrong JDK, which turns the version policy from convention into a gate — the same shift-left logic that applies to dependency policy, where a dependency scanning gate fails the build on known-vulnerable libraries instead of hoping developers read advisories.

One last habit: switching versions is also how you retire versions. When the last project pinned to an EOL JDK migrates, uninstall it (brew uninstall openjdk@11, sdk uninstall java <id>, and remove leftovers from /Library/Java/JavaVirtualMachines). A version manager full of dead runtimes is clutter; dead runtimes still on disk and discoverable are risk.

FAQ

What is the fastest way to switch Java versions on a Mac?

export JAVA_HOME=$(/usr/libexec/java_home -v 17) switches the current shell immediately, provided the JDK is installed and registered. For frequent or per-project switching, jenv or SDKMAN automate this.

Does jenv install Java?

No. jenv only switches between JDKs that are already installed (via Homebrew, installers, etc.) and registered with it using jenv add. If you want a tool that also downloads JDKs, use SDKMAN.

How do I pin a Java version to a single project?

With jenv, run jenv local 17 in the project root to write a .java-version file. With SDKMAN, run sdk env init to create .sdkmanrc. Commit either file so teammates and tooling pick up the same version.

Why do java -version and Maven show different Java versions?

They resolve Java differently: java follows your PATH (shims included), while Maven and Gradle typically follow JAVA_HOME. Enable jenv's export and maven plugins, or ensure your switcher sets JAVA_HOME and not just PATH, and the two align.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.