Safeguard
DevSecOps

How to Install Java on a Mac with Homebrew (Safely and Cleanly)

The fastest way to install Java on Mac: brew install openjdk, one symlink so macOS can find it, and JAVA_HOME set correctly — plus how to keep the JDK patched afterward.

Karan Patel
Platform Engineer
6 min read

To install Java on Mac with brew, run brew install openjdk (or a pinned LTS like brew install openjdk@21), symlink it into /Library/Java/JavaVirtualMachines so macOS's java_home can see it, and set JAVA_HOME in your shell profile. That is the whole procedure — three commands — but each step has a reason, and skipping the symlink is why half the "I installed Java but java -version says command not found" threads exist. This guide covers the clean path, the choice between Homebrew's OpenJDK builds and Temurin, and the part most tutorials skip: keeping the JDK you installed patched.

Why brew, and not a downloaded .dmg

You can absolutely install java using brew alternatives — Oracle's installer, Adoptium's .pkg, or a vendor download. Homebrew wins for developers on three grounds:

  • Updates are one command. brew upgrade patches your JDK along with everything else. JDKs get quarterly security updates (CPU releases in January, April, July, October); an installer-based JDK is patched exactly as often as you remember it exists, which in practice is never.
  • Versions are managed, not scattered. Multiple JDKs coexist under Homebrew's prefix instead of accumulating in /Library/Java from years of one-off installers.
  • Uninstalls are real. brew uninstall openjdk@17 actually removes it.

The security argument matters more than people credit. An unpatched JRE from 2023 sitting in your PATH is the java mac install equivalent of an unpatched browser — deserialization and TLS fixes ship in those quarterly updates.

Step 1: install the JDK

For the latest OpenJDK:

brew install openjdk

For a specific LTS release — the right choice for most project work, since frameworks and build tooling target LTS lines (17 and 21 are the current LTS versions as of this writing):

brew install openjdk@21
# or
brew install openjdk@17

Prefer a vendor-built distribution with its own support lifecycle? Eclipse Temurin (the Adoptium project's builds) installs as a cask:

brew install --cask temurin
# pinned major version:
brew install --cask temurin@21

Difference in behavior worth knowing: the openjdk formulae are keg-only — Homebrew does not link them into your PATH or register them with macOS, to avoid shadowing Apple's Java stubs. The Temurin cask, by contrast, installs a full .pkg into /Library/Java/JavaVirtualMachines, so it is visible to macOS immediately with no symlink step.

Step 2: the symlink (the step everyone misses)

macOS discovers JDKs through /usr/libexec/java_home, which scans /Library/Java/JavaVirtualMachines. Because Homebrew's openjdk is keg-only, you connect it manually — brew prints this exact command in its post-install caveats:

# Apple Silicon
sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk \
  /Library/Java/JavaVirtualMachines/openjdk-21.jdk

# Intel Macs use /usr/local instead of /opt/homebrew

Verify macOS can now see it:

/usr/libexec/java_home -V

You should get a list of installed JDKs with versions, vendors, and paths. If your JDK is missing from this list, the symlink is wrong — that, or you installed openjdk but are checking before linking.

Step 3: JAVA_HOME and PATH

Build tools (Maven, Gradle) and IDE launchers key off JAVA_HOME. Set it from java_home rather than hard-coding a Homebrew cellar path that changes on upgrade:

# ~/.zshrc
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
export PATH="$JAVA_HOME/bin:$PATH"

Then source ~/.zshrc and confirm:

java -version
# openjdk version "21.0.x" ...
echo $JAVA_HOME

Using java_home -v 21 means "give me whatever 21.x is currently installed," so quarterly patch upgrades never break your environment.

Multiple Java versions

Installing several JDKs side by side is normal — one legacy service on 17, new work on 21. Install each formula, symlink each, and switch by changing which version java_home resolves:

jdk() { export JAVA_HOME=$(/usr/libexec/java_home -v "$1"); }
# usage: jdk 17   /   jdk 21

If you switch constantly or need per-project pinning, a version manager (jenv or SDKMAN) is the better tool — we cover that workflow in a companion post on changing Java versions on a Mac.

Keeping it patched (the part that matters after day one)

The install is not the finish line. Two habits:

brew update && brew upgrade

Run it routinely (or let a scheduled task do it). JDK CPU releases fix real vulnerabilities; the whole point of a package-managed brew java setup is that patching is free.

And know what your projects run, not just your laptop. Your machine having a patched JDK does nothing for the CI image still building on an EOL Java 8, or the app pulling a vulnerable Jackson version. Runtime versions and dependency versions are separate risk surfaces — the latter is what software composition analysis exists for, and a scanner such as Safeguard will flag both an outdated dependency line and the known CVEs attached to it long before a pentest would.

Troubleshooting quick hits

  • java: command not found after install — keg-only strikes again: either add the symlink (step 2) and set PATH via JAVA_HOME, or follow brew's caveat to prepend the keg's bin directory to PATH.
  • java_home shows the wrong version first — it lists the highest version by default; pin explicitly with -v.
  • "The operation couldn't be completed. Unable to locate a Java Runtime." — macOS's stub /usr/bin/java found no registered JDK; the symlink is missing.
  • Apple Silicon vs Intel paths — Homebrew lives at /opt/homebrew on Apple Silicon and /usr/local on Intel. Copy-pasted snippets fail across architectures for exactly this reason; $(brew --prefix) writes portable scripts.
  • Rosetta duplication — an x86_64 brew under Rosetta plus a native arm64 brew means two independent JDK sets. Check arch and which brew is first in PATH.

Uninstalling cleanly

brew uninstall openjdk@21
sudo rm /Library/Java/JavaVirtualMachines/openjdk-21.jdk   # remove your symlink
/usr/libexec/java_home -V                                   # verify it is gone

Old installer-based JDKs from previous jobs and tutorials tend to lurk in /Library/Java/JavaVirtualMachines — while you are in there, delete anything you no longer need. Every stale JDK is an unpatched runtime someone might accidentally target a build at.

FAQ

What is the single command to install Java on a Mac with Homebrew?

brew install openjdk for the latest OpenJDK, or brew install openjdk@21 for the current LTS. Follow it with the symlink command brew prints in its caveats so /usr/libexec/java_home can find the JDK, then set JAVA_HOME.

Should I install openjdk or Temurin via brew?

Both are solid OpenJDK builds. Homebrew's openjdk formulae are keg-only and need a manual symlink; the temurin cask installs a vendor .pkg that macOS registers automatically. Teams standardizing on a supported distribution often pick Temurin; either is fine for development.

Why does java -version fail right after brew install openjdk?

Because the formula is keg-only: Homebrew deliberately does not link it into your PATH or register it with macOS. Add the symlink into /Library/Java/JavaVirtualMachines and export JAVA_HOME from /usr/libexec/java_home, and it resolves.

How do I update Java installed with brew?

brew update && brew upgrade. Version-pinned formulae like openjdk@21 receive patch updates within their line, which is exactly what you want for the quarterly JDK security releases.

Never miss an update

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