Managing Multiple JDKs on macOS
Prologue
Installing JDKs wasn’t something I needed to do often in the past - after all, Java releases were usually 3-4 years apart. Recently, however, this has changed1 and I find myself installing a new JDK every 6 months. There’s also the problem that now I have to switch between more JDKs to test things…
I did a bit of research into streamlining my workflow with multiple JDKs and I thought I might share some of my findings with you. My workflow is macOS-specific, but I think that users of other Unix-like operating system can easily adapt it to their environment.
Another thing to note about my workflow is that it’s very lightweight and doesn’t depend on the usage of any third-party tools. You won’t find here anything about using SDKMAN! and jEnv. You can think of this post as something in the spirit of the awesome talk Programming with Hand Tools - sometimes the journey is way more important than the destination itself.
Installation
I wasn’t always a macOS user. Actually during most of my career I was actually a GNU/Linux user.2 While I really like macOS and I find it to be the most balanced (in terms of development and desktop experience) OS out there, my transition from Linux wasn’t painless. Especially when it came to setting up my preferred programming environment…
When I was a GNU/Linux user, I always installed Java (and pretty much
everything else) via my distro’s package manager (e.g. dnf
) . Most
distros had in their repositories the last 2-3 JDK releases and this
was everything I needed. What’s more - Linux distros typically
provided some mechanism to switch between the JDKs you had installed
(e.g. Fedora has the alternatives
program).
When I transitioned to macOS I really missed a proper package manager
and such version management utilities. It felt so weird to have do
download and install libraries and applications manually. Sure,
there’s homebrew
, but it’s not quite like dnf
, apt
, pacman
,
and friends. It’s getting better each year, though. However, I never
installed Java through brew
for some reason. Maybe I just lost my
console edge or something like this, but I’d simply to go Oracle’s
website, download the latest image, double-click it and be done with
it. Things just worked.
Now, however, the OracleJDK’s terms of use have changed (you can only use it for development purposes without paying for it), and I decided I’d better switch my development environment to OpenJDK, so it’d match whatever I’m using in production down the road.
The OpenJDK doesn’t have a convenient dmg
installation image - it’s
distributed simply as a tarball. No problem! Installing this tarball
is trivial:
$ tar xf tar xf openjdk-11.0.1_osx-x64_bin.tar.gz
$ sudo mv jdk-11.0.1.jdk /Library/Java/JavaVirtualMachines
You just extract it and move it where macOS expects JDKs to be installed. You can check that now macOS picks up the right version of Java:
$ java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
Behind the Curtains
Have you ever wondered what happens to your old JDK once you’ve
installed a new one? Well, they just stick around in
/Library/Java/JavaVirtualMachines
. How convenient! You can easily
check what JDKs are available there either by doing a simple ls
or by
using the java_home
wrapper command:
$ ls /Library/Java/JavaVirtualMachines
jdk-10.0.1.jdk jdk-11.0.1.jdk jdk1.8.0_131.jdk
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (3):
11.0.1, x86_64: "OpenJDK 11.0.1" /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home
10.0.1, x86_64: "Java SE 10.0.1" /Library/Java/JavaVirtualMachines/jdk-10.0.1.jdk/Contents/Home
1.8.0_131, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home
When you’ve got multiple JDKs, by default the macOS Java wrapper
commands (e.g. java
, javac
, etc) would just use whatever’s the
newest JDK, but you can also easily switch between the installed JDKs.
Switching between JDKs
The simplest way to switch between JDKs is to set JAVA_HOME
to whatever JDK you want to use:
# switch to Java 8
$ export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home
$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
# go back to the default
$ unset JAVA_HOME
$ java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
You don’t really need to remember the exact path to each installed JDK, as java_home
can help you with this:
$ /usr/libexec/java_home -v 1.8
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home
# switch to Java 8
$ export JAVA_HOME=$(/usr/libexec/java_home -v1.8)
$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
Notice the version scheme change after Java 8 - up to it JDK versions
were internally 1.x, but since JDK 9 they became 9, 10, and so on. In
practical terms this means you can’t pass -v 8
to java_home
- you
have to pass -v 1.8
instead.
Armed with this knowledge, you can devise some helpful shell function like this:
switch_java() {
echo "Switching to Java $1"
export JAVA_HOME=`/usr/libexec/java_home -v $1`
java -version
}
Add this to your shell config (e.g. .bashrc
), reload the config
(e.g. by using . .bashrc
), and use it like this:
$ switch_java 1.8
Switching to Java 1.8
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
$ switch_java.sh 11
Switching to Java 11
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
I’ll leave it as an exercise for the readers to extend this function to show you all available JDKs and some indication which one is the currently active. Such modification will be fairly straightforward.
Epilogue
That’s all, folks! Hopefully you enjoyed this post and learned something useful from it!
In the modern age software engineers reach for third-party solutions for trivial things and I find this a bit troubling. That’s really the entire point of this post - rolling up your sleeves and getting dirty can be fun and rewarding. You get to learn more, you get to build a workflow that’s tailored to your needs, and you get to have some fun in the process. That makes you feel like a real hacker, doesn’t it?
Journey before destination…