Published on

MacOS JDK path

  • Name

An overlook of the JDK path on MacOS.


There're several symlinks targeted to system java lib directory from brew-installed JDK. And JEnv could help to mange and switch multiple JDK versions.

MacOS Library folder levels

On MacOS system, there's 3 levels of Library folder in simply terms:

  • The user library, ~/Library, stores per-user settings etc.
  • The local library, /Library, stores computer-wide settings etc.
  • The system library, /System/Library, stores the base settings, resources, etc that comes with the system. (In theory, you shouldn't change anything in here.)

JDK path

Normally, the JDK path lays on /Library/Java/JavaVirtualMachines/. However, if JDK installed via IntelliJ, it will be ~/Library/Java/JavaVirtualMachines/.

brew install

Things become little complicated when comes to Homebrew. First, let's install the latest version of JDK via brew.

$ brew install openjdk
# or (the java is an alias for openjdk)
$ brew install java

By the way, the JDK migrated from homebrew/cask to homebrew/core, and brew 4 replaced the git repo with JSON file for metadata, you don't have to tap any taprooms unless you want to install a non-LTS version(in such case, you need homebrew/cask-versions) or from other third-party vendors.

After successfully installed, brew will prompts something like this:

==> Caveats
For the system Java wrappers to find this JDK, symlink it with
  sudo ln -sfn /usr/local/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

openjdk is keg-only, which means it was not symlinked into /usr/local,
because macOS provides similar software and installing this software in
parallel can cause all kinds of trouble.

If you need to have openjdk first in your PATH, run:
  echo 'export PATH="/usr/local/opt/openjdk/bin:$PATH"' >> ~/.zshrc

For compilers to find openjdk you may need to set:
  export CPPFLAGS="-I/usr/local/opt/openjdk/include"
==> Summary
🍺  /usr/local/Cellar/openjdk/20.0.1: 636 files, 322.4MB

As suggested, we need to run sudo ln -sfn /usr/local/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk to symlink our installed JDK to system Java wrapper. How this works? That command just creates a soft link from JDK installation to system java lib folder. But when we run which java, it shows like this:

$ which java

What is /usr/bin/java? Where does the JavaVirtualMachines folder go?

The /usr/bin/java is system wrapper tool that locates and executes the actual java executables, which means it will look for available java versions under JavaVirtualMachines folder. And MacOS system provides a tool to locate the effective JAVA_HOME, /usr/libexec/java_home.

Ok, that explained something, but how about the line shown the real JDK installation path? Why /usr/local/Cellar/openjdk instead of /usr/local/opt/openjdk?

Well, it's symlink again. Roughly as the purpose of JavaVirtualMachines folder for multiple versions, Homebrew installs formulae to the Cellar at $(brew --cellar) and then symlinks some of the installation into the prefix at $(brew --prefix). We can see the linking like this:

$ ls -l /Library/Java/JavaVirtualMachines/openjdk.jdk
lrwxr-xr-x  1 root  wheel    42B Aug 15  2020 /Library/Java/JavaVirtualMachines/openjdk.jdk -> /usr/local/opt/openjdk/libexec/openjdk.jdk
$ ls -l /usr/local/opt/openjdk
lrwxr-xr-x  1 kid  admin    24B May 20 15:46 /usr/local/opt/openjdk -> ../Cellar/openjdk/20.0.1
$ ls -l /usr/local/Cellar/openjdk/20.0.1
total 56
-rw-r--r--   1 kid  admin   3.5K May 20 15:46 INSTALL_RECEIPT.json
-rw-r--r--   1 kid  admin    19K Mar  7 21:51 LICENSE
-rw-r--r--   1 kid  admin   424B Mar  7 21:51
drwxr-xr-x  31 kid  admin   992B Mar  7 21:51 bin
drwxr-xr-x  10 kid  admin   320B Mar  7 21:51 include
drwxr-xr-x   3 kid  admin    96B Mar  7 21:51 libexec
drwxr-xr-x   3 kid  admin    96B Mar  7 21:51 share

The folders are different for Apple Silicon machines, it would be /opt/homebrew/Cellar instead of /usr/local/Cellar, and /opt/homebrew/opt instead of /usr/local/opt.

Wrap up: java -> /Library/Java/JavaVirtualMachines/openjdk.jdk -> /usr/local/opt/openjdk -> /usr/local/Cellar/openjdk


JEnv is a CLI tool to facilitate managing and switching multiple JDK versions on MacOS/Linux.

Quickly get started:

$ brew install jenv
$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(jenv init -)"' >> ~/.bash_profile
$ jenv enable-plugin export # to export JAVA_HOME
# add JDK to Jenv
$ jenv add /Library/Java/JavaVirtualMachines/openjdk.jdk/
# list all managed jdk versions
$ jenv versions
* system (set by /Users/kid/.jenv/version)
# config a global version
$ jenv global 20.0
# config a local(per-directory) version
$ jenv local 20.0
# config a (current) shell version
$ jenv shell 20.0

We can now the JEnv script is taking over the control of responsibility to locate Java executables. Yes, just another wrapper, another abstract layer.

$ which java

JEnv provides more dynamical and fine-grained control of locating java. Essentially, it looks different registered Java versions under ~/.jenv/versions by given condition. For each specifying version command, JEnv will export the corresponding JAVA_HOME env var. Hence each time firing java, the JEnv shim will redirect to the specific java executable (by default, is the system Java wrapper).

$ jenv which java
$ jenv shell 20.0
$ jenv which java