September 21, 2026 ยท Guide

How To Run Two Java Versions On One Windows Machine

Every Java dev hits this eventually. You’re maintaining a Spring Boot service pinned to Java 17 because that’s what the client’s stack runs, and you also want to try the new language stuff in Java 23. Or you inherited a build script from 2019 that chokes on anything newer than 8, and now you have to keep it building while you write modern code on the same laptop. Installing one JDK means the other breaks. Installing both means java -version in any terminal gives you whichever one happened to win the last battle in PATH.

You can absolutely run two Java versions on one Windows machine at the same time. The trick is not really about installing. It’s about controlling which JDK answers when you type java, in each shell, per project, without editing system settings every five minutes.

Get both JDKs installed, and don’t touch PATH yet

Installing multiple JDKs on Windows is trivial. The friction begins when installers try to be helpful and rewrite JAVA_HOME and your Path for you. Two separate installs will fight over that. So the goal is: install both, disable anything automatic, and let the shell decide at runtime.

Grab the vendor builds you want. Oracle JDK if you’re licensed for it, otherwise Eclipse Temurin, Microsoft Build of OpenJDK, or Azul Zulu all ship clean Windows MSIs and ZIPs. Temurin is my default recommendation for devs because the installer lets you skip PATH and JAVA_HOME changes entirely, which is what you want here.

Put both under a predictable folder, not Program Files. Something like C:\jdks\temurin-17 and C:\jdks\temurin-23. Version numbers in folder names are non-negotiable if you plan to do this for more than a month.

C:\jdks\
  temurin-17\
  temurin-23\

If an installer already wrote a Java entry into your machine-level Path, open “Edit environment variables for your account,” find it, and remove the java line. Do the same in System variables if it’s there. Now Windows doesn’t know about Java at all until you tell it. That’s fine. That’s the point.

Point JAVA_HOME at the default, then override per shell

Plenty of tools still read JAVA_HOME: Maven, Gradle, some IDEs, a worrying number of installers. Set it once at the user level to whichever version you want as the default. I keep 17 as the default because half the projects on my drive expect it.

Then, for any session that needs a different JDK, override it. In PowerShell:

$env:JAVA_HOME = "C:\jdks\temurin-23"
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
java -version

In an old-school cmd.exe window:

set JAVA_HOME=C:\jdks\temurin-23
set PATH=%JAVA_HOME%\bin;%PATH%
java -version

The %JAVA_HOME%\bin prefix is the whole secret. By putting it at the front of PATH inside the shell, you win the lookup race. When you close the window, it’s gone. Your system default is untouched.

Two warnings. First, never set JAVA_HOME to the bin folder. Tools append \bin themselves and will look for C:\jdks\temurin-23\bin\bin\java.exe, which doesn’t exist. Second, avoid JAVA_HOME paths with spaces or ampersands. Some batch scripts and older build wrappers fall apart on those.

Make it painless with a tiny profile function

Typing those two lines on every new tab gets old fast. I keep a small function in my PowerShell profile that swaps the version by name. Create or open $PROFILE (usually ~\Documents\PowerShell\Microsoft.PowerShell_profile.ps1 or the WindowsPowerShell variant) and add:

function Use-Jdk {
    param([string]$Version)
    $root = "C:\jdks\temurin-$Version"
    if (-not (Test-Path "$root\bin\java.exe")) {
        Write-Host "No JDK at $root" -ForegroundColor Red
        return
    }
    $env:JAVA_HOME = $root
    $env:Path = "$root\bin;" + (($env:Path -split ';' | Where-Object { $_ -notmatch 'jdks' }) -join ';')
    & "$root\bin\java.exe" -version
}

Then Use-Jdk 23 or Use-Jdk 17 flips the current window. The PATH-filtering line strips any older JDK entry first, so you don’t end up concatenating paths forever. Reload with . $PROFILE after editing.

Now the harder part: your IDE and your build tool

The shell is only half the story. IntelliJ IDEA and VS Code both have their own JDK settings that don’t care about your terminal. In IntelliJ, that’s File > Project Structure > Project SDK per project, plus Settings > Build, Execution, Deployment > Build Tools > Maven > Runner > JRE for the build process. Set the project SDK and leave the global default alone. One project on 17, another on 23, same IDE instance, no conflict.

Maven and Gradle are simpler if you trust JAVA_HOME. Maven reads it and uses it for both compile and run. Gradle honors org.gradle.java.home in gradle.properties if you want a per-project pin that doesn’t depend on the shell.

org.gradle.java.home=C:/jdks/temurin-17

Forward slashes matter here. Backslashes in that file are parsed as escape sequences and break the path silently, which cost me an afternoon once.

Where this all falls apart

A few things genuinely annoy me about the Windows side of this.

  • System-level JAVA_HOME wins over user-level in weird ways depending on which process reads it. Services and some installers read System variables, your terminal reads User. Keep the two consistent or you’ll get confusing bugs.
  • Some installers force a specific JDK. Oracle’s installer historically nudges Path back to itself during updates. Re-check after every major update.
  • Git Bash and WSL don’t care about your Windows env the way you’d expect. WSL has its own JDK story entirely. If you’re bouncing between the two, don’t assume your PowerShell JAVA_HOME reaches into a bash session.

None of that is fatal. It just means: don’t trust the machine, trust the terminal you’re in and check java -version before you run the build. That habit alone prevents 90% of “why is this compiling with the wrong JDK” grief.

Verification before every build

Here’s the routine I’ve settled on after years of this. Before running mvn or gradle, I glance at three things: java -version in the active shell, the JAVA_HOME value, and the project’s configured SDK in the IDE. If all three agree, the build works. If any disagrees, I know exactly which knob to turn.

You don’t need containers, SDKMAN on WSL, or some elaborate directory-switching scheme to do this. You need two JDKs in predictable folders, an implicit system default, and a per-shell override that you control. That’s it. The rest is habit.