Set Up Java Properly: JDK, Environment Variables, and Your First Program
The first step in Java is not writing code — it is putting three things in the right places: a translator, an execution engine, and a lookup directory. Your .java file reads fine to humans and not at all to a machine, so a compiler (javac) translates it into bytecode (.class); that bytecode runs on any machine with a JVM, so a virtual machine (java) executes it on the spot; and neither command sits somewhere your terminal can see by default, so two environment variables have to say where they live. That is the whole article: what to install, how the system finds it, and what happens inside one command once it is found.
think of the JDK as a university library. JAVA_HOME is the street address of that library registered with the city — Maven, Gradle and IDEA are the delivery people who look up that address to collect books. PATH is "which floor the borrowing desk is on" — when you walk in yourself (typing java), the front desk uses that to point you at the window. Whether you write the address as "the street" or as "the room holding the window" is exactly where beginners crash.

After this article you should be able to answer three questions:
- Am I installing a JDK or a JRE, and why does nobody install a standalone JRE anymore?
- Who reads
JAVA_HOMEand who readsPATH? What error does each mistake produce? - When you type
java Hello, which five things happen inside the machine, in order?
Almost every beginner gets confused here: one guide tells you to install the JDK, another mentions the JRE, and your task manager shows something called a JVM. These are not three parallel things — they are nested, like Russian dolls.

| Term | Stands for | In one sentence | What's inside |
|---|---|---|---|
| JVM | Java Virtual Machine | The virtual computer that actually executes programs | Interpreter, JIT, GC, memory model |
| JRE | Java Runtime Environment | The minimum needed to run Java programs | JVM + core libraries |
| JDK | Java Development Kit | The complete toolkit for developing Java | JRE + javac / jar / javadoc |
to merely run someone else's program, a JRE is enough. To write even a single line of Java, you need the JDK. Modern JDK installers already bundle a JRE, so stop hunting for a separate JRE download page.
Java ships a new release every six months, but not every release deserves production use. Only LTS releases are meant for production — that is an industry rule:
| Version | Type | Support until | Recommendation |
|---|---|---|---|
| Java 8 | LTS (ancient) | 2030 | Legacy maintenance only |
| Java 11 | LTS | 2026 | Transitional choice |
| Java 17 | LTS | 2029+ | The minimum for Spring Boot 3 |
| Java 21 | LTS | 2031+ | Best default for new projects; virtual threads are stable |
Spring Boot 3.x hard-requires JDK 17. Open a Boot 3 project with JDK 8 and it throws UnsupportedClassVersionError within the first second — with a very long, very scary message.

Downloading from Oracle's site works, but version management gets painful. Use winget or Scoop instead — one command each:
# Option 1: winget (built into Win10 1809+)winget install EclipseAdoptium.Temurin.21.JDK# Option 2: Scoop (more flexible version switching)scoop bucket add javascoop install temurin21-jdkAfter installation you do not need to configure environment variables by hand — package managers register everything. Manual zip installs do require Section 3.
brew install --cask temurin@21# Point JAVA_HOME at itecho 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrcsource ~/.zshrcmacOS ships /usr/libexec/java_home, a built-in JDK locator that is smarter than a hard-coded path — it can pick a version when several are installed.
# Debian / Ubuntusudo apt update && sudo apt install -y openjdk-21-jdk# For freer version switching, use SDKMANcurl -s "https://get.sdkman.io" | bashsdk install java 21.0.2-temsdk use java 17.0.10-tem # switch to 17 temporarilyTip: on a server, java -version may still show a preinstalled older JDK. That means the old version sits earlier in PATH — run which java to see which one actually wins.
This is the step beginners most often copy wrong, and it is worth understanding properly. The two variables have completely different jobs:
JAVA_HOME = D:\dev\jdk-21 # tells TOOLS where the JDK livesPATH += %JAVA_HOME%\bin # tells the SHELL where to find java- Who reads JAVA_HOME? Tools — Maven, Gradle, Tomcat, IDEA — to locate a JDK.
- Who reads PATH? The command shell: when you type
java, it walks the PATH directories one by one.

The two chains never talk to each other, which is why "who complains when this is wrong" is impossible to memorise. Play a round instead: pick a setting on the left, then its actual job on the right — a wrong pick explains itself on the spot.
the JVM looks for a class the way you look for a book at university — you check the main library first. It asks the central stacks (the class library shipped with the JDK); only if the main library says "we don't hold that" does it try the departmental branch (the jars and folders listed in -cp). This "ask upstairs first, search yourself only if they don't have it" rule is called parent delegation, and it guarantees that a String.java you write in your own project can never overwrite the JDK's String.
setting JAVA_HOME to D:\dev\jdk-21\bin is the all-time classic. Tools append \bin\java themselves, producing bin\bin\java, and Maven fails with "JAVA_HOME is set to an invalid directory".
To verify, run one command — and read the capitalized version number:
$ java -versionopenjdk version "21.0.2" 2024-01-16 LTSOpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS)OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode)- The three lines are: JDK version, runtime environment, VM implementation
64-Bit Server VMmeans 64-bit server mode — the production standard- If it prints 1.8.0_xxx, your PATH still points at Java 8
Attention: both java -version (single dash) and java --version (double dash) work, but the first is legacy style and the second is proper GNU style.
Create a folder and type this in any text editor (save it as Hello.java, not the .txt Notepad suggests):
public class Hello { public static void main(String[] args) { System.out.println("Hello, Java!"); // main is the entry point the JVM expects: a fixed signature for (int i = 0; i < args.length; i++) { System.out.println("arg " + (i + 1) + ": " + args[i]); } }}Open a terminal in that folder and run two commands:
# Step 1: compile source into bytecodejavac Hello.java # produces Hello.class# Step 2: run it — the JVM loads and executes the classjava Hello alice bob # note: no .class suffixjavac Hello.javainvokes the compiler and emitsHello.class— platform-independent bytecodejava Hellostarts a JVM, loads Hello.class, and executes main- Command-line arguments land in
String[] argsand get printed by the loop
Why the .class step? Because this is exactly how Java achieves portability: compile once, and the bytecode runs anywhere a JVM exists — that is what "write once, run anywhere" actually means.
java Hello.class is wrong. The java command always takes a class name (Hello), never a file name. Four extra characters and the JVM reports it cannot find the class.
Those five stages (find the command → load the JVM → build the classpath → load the main class → call main) read like a rhyme, but failures always land in one specific box. Spread them out as a single-step run: the six lines on the left, and on the right the variables and call stack as they change. Press step repeatedly and watch step 4 — that is where the Section 9 error is born:
javac Hello.java # the shell walks PATH and finds javacjava Hello # same rule, this time it finds java.exe// java.exe is only a launcher: it pulls jvm.dll / libjvm.so into the process// build the classpath: -cp first, then the CLASSPATH variable, then the current dir// load Hello.class by its fully qualified name and turn it into a Class object// invoke public static void main(String[])| command looked up | javac |
| decided by | the order of PATH |
| output | Hello.class |
shell → PATH → javac.exeIn real work you may maintain a JDK 8 system in the morning and build a JDK 21 project in the afternoon. Never uninstall and reinstall to switch — use a manager:
| Tool | Platform | Key command | Best for |
|---|---|---|---|
| SDKMAN | macOS / Linux | sdk use java 17.0.10-tem | Command-line users |
| jenv | macOS / Linux | jenv global 21 | Per-project configs |
| winget + manual PATH | Windows | Drag entries in the env dialog | Occasional switching |
| IDEA's JDK dropdown | All | File → Project Structure | Switching inside the IDE only |
The hard part of running several JDKs is not installing them but knowing which one is actually in charge, because three parties each follow their own rule: the shell follows PATH order, tools follow JAVA_HOME, the IDE follows its Project SDK. This animation runs the three lines side by side — pay attention to frame 6, the moment they disagree, which is exactly when "works in the IDE, dies in the terminal" is born:

"Don't let a laptop decide the build" boils down, in practice, to a few lines of pom. Rather than copying someone's, tick the boxes and watch what it emits — especially the java.version line and the three scopes:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>| Symptom | Root cause | Fix |
|---|---|---|
'javac' is not recognized | PATH missing or stale | Reopen the terminal; ensure PATH contains %JAVA_HOME%\bin |
JAVA_HOME is set to an invalid directory | JAVA_HOME includes \bin | Point it at the JDK root |
Error: Could not find or load main class Hello | Class and file names differ / ran the .class | Make them match; java Hello |
UnsupportedClassVersionError | Old JDK running newer bytecode | Upgrade the JDK to 17+ |
unmappable character for encoding | Non-ASCII source without an explicit charset | javac -encoding UTF-8 Hello.java |
| Runs in IDEA, fails in the terminal | The two use different JDKs | Compare which java with IDEA settings |
Environment setup is hard to remember because it is path lookup you cannot see. These four kernel experiments really execute the JDK's layers in your browser, and every one lets you flip the parameter yourself.
The first unpacks "the life of one line of code". Bytecode is the intermediate language javac emits; a class loader is the role that reads .class files into memory; JIT (just-in-time) compilation is the accelerator that turns hot bytecode into machine code once the JVM notices a loop running often. Switch to "JIT tiers" and you watch the same loop get interpreted first and compiled later — that is why Java feels slow at startup and fast afterwards:
The second answers the analogy from Section 3 head-on: why a String.java of yours can never beat the JDK's String. Pick "parent delegation" to watch a request climb upward; pick "duplicate jars" to see which copy wins when two jars hold the same class — the one listed earlier in the classpath, which is exactly the root cause behind the NoClassDefFoundError row in the Section 9 table:
Flip the same mechanism around and you get the error that breaks most beginners. On "ClassNotFoundException" you literally watch the JVM walk every directory on the classpath and come back empty-handed:
The third explains something you will meet constantly from the next article on: IDEA quietly does one job before Run. Under "compile output" you see .java become .class inside target/classes; run without compiling first and you are executing stale bytecode:
The fourth experiment answers Section 3's question directly: which JDK is actually working. On "who wins" you watch the shell walk PATH and take the first hit; on "JAVA_HOME vs PATH" the two readers separate; "several JDKs installed" is the lifelike one — 8, 17 and 21 coexisting, with tools, shell and IDE each picking their own:
The order of that lookup chain is not something you absorb by reading — you have to click. This diagram splits "main library first" into five boxes:
Finally, a preview of how an application dies when the environment is wrong. It is not a one-line 'class not found' but a full screen — the triage desk below trains exactly that reading:
Once you are bored of buttons, type the commands yourself. This console talks to the same in-browser Java kernel, and every reply is computed there — start with whoami to see which JDK is live, then work through the lab envpath lines:
run lab envpath home followed by lab envpath fix — the first reproduces the trailing-\bin mistake, the second gives the verification order. Same error: first understand why it happens, then confirm it is really gone.

Steps 1 and 3 of that chain are both things you can write wrong. Change the address format on the left and the right panel immediately says who reads it and what it throws; then pick a JDK version below to see where Spring Boot and the language features each draw their line:
mvn -v: OKSpring Boot 3.x: OKswitch pattern matching available
the cell beginners overlook most is "jre-only". Oracle stopped shipping a standalone JRE from JDK 11, so any "JRE" you download today is a trimmed JDK — when javac is not recognized, first check whether your copy ships a compiler at all (look for javac inside bin).
Every phrase in the first column can be pasted straight into a search box — do not paraphrase or shorten it:
| Error text (excerpt) | Real cause | 30-second fix | Dig deeper in |
|---|---|---|---|
错误: 找不到或无法加载主类 Hello / Error: Could not find or load main class Hello | You passed a file name instead of a class name, the .class is not on the classpath, or the package and folder disagree | Use java Hello (no .class); confirm the bytecode exists with dir target\classes | Sections 4 & 6 · #3 IDEA project |
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0 | A JDK 8 launcher trying to run bytecode compiled by JDK 17 (61 = 17, 52 = 8) | Align java -version with the Java version printed by mvn -v; move both to 17+ | Section 3 · #36 packaging |
The JAVA_HOME is not defined correctly / JAVA_HOME is set to an invalid directory | JAVA_HOME unset, pointing at bin, or pointing at a JRE-only folder | Point it at the JDK root (e.g. D:\dev\jdk-21) and reopen the terminal, then echo %JAVA_HOME% | Section 3 · sandbox above |
'mvn' is not recognized as an internal or external command ('mvn' 不是内部或外部命令) | Maven's bin is not on PATH, or you edited PATH without reopening the terminal | Add %MAVEN_HOME%\bin to PATH, open a new window, verify with mvn -v | #2 Maven basics |
'java' is not recognized as an internal or external command | PATH lacks %JAVA_HOME%\bin | Append it to PATH; on Windows drag the entry up so it wins over older ones | Section 3 |
错误: 编码 GBK 的不可映射字符 (unmappable character for encoding GBK) | UTF-8 source containing Chinese, compiled with the platform default charset | javac -encoding UTF-8 Hello.java; set UTF-8 globally in IDEA | #35 logging · #3 IDEA |
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/Foo | Present at compile time, absent at runtime — the jar is missing from the runtime classpath | java -cp "target/classes;lib/*" com.example.Main | Section 7 classpath labs |
Could not find or load main class fuses two different causes — the class file was not found (a path/classpath problem) and it was found but failed to load (a version mismatch, or an exception thrown in a static initializer). When you cannot tell which, rerun with -verbose:class and the JVM prints where every class came from.
Version mismatch is the most common half of that fused message. Here is a real stack — do not read the answer, just click the frame you think is guilty:
You build the jar locally on JDK 21 and hand it to a CI box that only has JDK 8. The process never reaches main.
Goal: walk the whole "compile → run → break it on purpose → read the error" chain in a plain terminal, with no IDE involved.
// EnvCheck.java — drop it in any empty folder; the file name MUST be EnvCheck.javapublic class EnvCheck { public static void main(String[] args) { System.out.println("java.version = " + System.getProperty("java.version")); System.out.println("java.home = " + System.getProperty("java.home")); System.out.println("user.dir = " + System.getProperty("user.dir")); System.out.println("classpath = " + System.getProperty("java.class.path")); for (int i = 0; i < args.length; i++) { System.out.println("arg[" + i + "] = " + args[i]); } }}Run these in order and compare the output:
javac EnvCheck.javajava EnvCheck alice bobjava -cp . EnvCheck # spell out the classpath, so -cp stops being magicjava EnvCheck.class # ← break it deliberately and read the errorExpected output (the version line follows your own JDK):
java.version = 21.0.2java.home = D:\dev\jdk-21user.dir = D:\tmp\envcheckclasspath = .arg[0] = alicearg[1] = bobDone when: all four property lines print, and you can say out loud why java never takes a .class suffix.
Three tiny edits, one variable at a time — note what changes each time:
- Temporarily rewrite
JAVA_HOMEwith a trailing\binand runmvn -v. You will observe Maven failing withThe JAVA_HOME is set to an invalid directorywhilejava -versionstill works — they do not read the same thing. - Point the classpath somewhere wrong, e.g.
java -cp nonexistent EnvCheck. You will observeCould not find or load main class; replay the classpath experiment in Section 7 to see exactly which directories were walked. - Compile the same class twice into two folders (
javac -d out1 EnvCheck.java, edit one print line,javac -d out2 EnvCheck.java), then run with-cp "out1;out2"and with"out2;out1". You will observe the printed output following the classpath order — that is "the earlier duplicate jar wins".
Tip: after variant 3, go back to the "duplicate jars" mode in Section 7 and the abstract rule suddenly becomes concrete.
Write your own environment health check script — the thing you will thank yourself for on every new machine.
check-env.ps1on Windows,check-env.shon macOS / Linux- Print at least: the real
java -version,javac -version, the Maven and Java lines frommvn -v, the resolved path fromwhere java/which java, and whetherJAVA_HOMEpoints at a root rather thanbin - Each check prints either
[OK]or[FAIL] reason + one-line fix, and the script exits 0 only when everything passes - Bonus: detect multiple installed JDKs and list them all (
/usr/libexec/java_home -Von macOS,update-alternatives --list javaon Linux)
Acceptance checklist: ① removing java from PATH produces a FAIL instead of a crash; ② running it on two machines pinpoints "different JDK version" from the diff alone; ③ you, three months from now, still understand those FAIL messages.
state the JDK / JRE / JVM containment relation from memory, and explain why nobody installs a standalone JRE anymore.
who reads JAVA_HOME and who reads PATH? If JAVA_HOME ends in ...\bin, which tool complains and with what exact sentence?
why can't you write .class after java Hello? What does the JVM think your string is?
bytecode is what makes "write once, run anywhere" true — which layer is being made portable, source or machine code?
two jars contain the same class name. Which copy loads, and how does that match the "main library first" analogy?
the JDK supplies, JAVA_HOME locates, PATH hails, bytecode runs — tools to install, paths to find, the command you shout, and the platform-neutral class that actually executes.
this article boils down to three facts — the JDK contains the JRE which contains the JVM; JAVA_HOME points at the root while PATH points at bin; let tools manage versions instead of manual hacking. Get these right and no later tutorial will ever be interrupted by environment issues again.