Topic 6 of 119 1. Introduction to Java Interactive Tutorial

JDK Explained: Components, Setup Process, and Installation Guide

20 mins read Last updated: 22 Aug 2026 16,038 views

Direct Answer & Core Concept

The JDK (Java Development Kit) is the complete toolchain for building Java software. It supplies the compiler `javac` for translating source to bytecode, the launcher `java` for running it, and about 26 other tools covering packaging, inspection and diagnostics, including `jar`, `javap`, `jlink`, `jshell`, `jcmd`, `jfr`, `jstack` and `jdeps`. The most important correction is that **you no longer install a JRE separately**. The standalone JRE distribution was removed in JDK 11, because the runtime now lives inside the JDK as part of the module system, and `jlink` can build a trimmed runtime from just the modules you need. On this machine the full Temurin 17 JDK is **318 MB** while a `jlink` image containing only `java.base` is **37 MB** and runs a compiled class perfectly well. Setup boils down to installing an LTS release, setting `JAVA_HOME` to the JDK root, putting `$JAVA_HOME/bin` on your `PATH`, and then confirming that `java -version` and `javac -version` report the **same** version from the **same** directory, since a mismatch between the two is the most common cause of confusing failures. Note also that `JAVA_HOME` is an environment variable you set and may be `null`, whereas `java.home` is a system property the JVM always populates; since JDK 9 `java.home` points at the JDK root rather than a nested `jre/` directory, which silently broke build scripts that assumed the old layout.

Detailed Explanation & Mechanics

1. What the JDK Actually Is

The JDK is the toolchain required to *build* Java software, as distinct from the runtime required to *run* it. That distinction used to be crisp, because you installed a JDK and a separate JRE. **It no longer is**, and this is the single most important thing to understand about the modern JDK.

Since JDK 11 the separate JRE distribution no longer exists. There is no JRE installer to download, because the runtime is already inside the JDK and the **module system** lets you carve out exactly the parts you need. Any tutorial telling you to "download the JRE separately" is describing a workflow that ended years ago.

Architecture Diagram
WHAT PEOPLE STILL DRAW              WHAT IS ACTUALLY TRUE TODAY
   ┌────────────┐  ┌────────────┐      ┌──────────────────────────────┐
   │    JDK     │  │    JRE     │      │             JDK              │
   │ javac,jar  │  │  java      │      │  java, javac, jar, jshell    │
   │ javadoc    │  │  lib/*.jar │      │  jlink, jmod, jpackage, ...  │
   └─────┬──────┘  └─────┬──────┘      │                              │
         └──── contains ─┘              │  ~70 modules, incl. java.base│
              (true until 2018)         │  jlink builds a runtime image │
                                        └──────────────────────────────┘

The verifiable proof of this is on disk. Listing the contents of a real Temurin 17 installation shows there is no jre directory at all:

Main.java
Run Online
$ ls /tmp/jdkbin conf include jmods legal lib man NOTICE release

No jre/. What took its place is jmods, holding 70 module files, and the bin directory now carries 28 executables.

2. The Tools You Actually Get

A modern JDK ships 28 command-line tools. They are worth knowing by category, because most people use four of them and have never heard of the rest.

CategoryToolsPurpose
Buildjavac, javadoc, jdeprscanCompile, generate docs, find removed APIs
Runjava, jshellLaunch a program, or a REPL with no setup
Packagejar, jpackage, jmodJARs, native installers, module files
Inspectjavap, jdeps, jimageDisassemble bytecode, find dependencies
Diagnosejcmd, jstack, jmap, jfr, jps, jstatAttach to a running JVM to inspect it
Securitykeytool, jarsignerManage keystores, sign JARs
Runtime imagejlinkBuild a custom runtime from named modules

Two entries on this list are the ones worth calling out for beginners, because neither appears in any "getting started" tutorial.

**jshell** is a REPL. It runs Java interactively with no project, no file, and no main method, which is the fastest way to check how a language feature actually behaves:

Main.java
Run Online
$ jshelljshell> int x = 40 + 2;
jshell> System.out.println("answer is " + x);
answer is 42jshell> /exit

**jlink** builds a custom runtime image containing only the modules you name. This replaced the JRE download entirely, and the size difference is dramatic. On this machine:

Main.java
Run Online
full JDK : 318 MBjlink image, java.base only : 37 MB

That 37 MB image is a genuinely working runtime. It contains two executables, java and keytool, it lists exactly one module, and it runs a compiled class:

Main.java
Run Online
$ ./myrt/bin/java --list-modulesjava.base@17.0.20.1$ ./myrt/bin/java -cp out Tinytrimmed runtime works

3. `javac`: Compilation and the Release Flag

javac translates source into bytecode. The detail that matters for setup is **the release flag**, because it is what stops a build from silently producing class files a target machine cannot run.

Main.java
Run Online
javac --release 17 MyApp.java # bytecode targets Java 17

--release does two things at once: it sets the bytecode target version *and* restricts the API surface to that release's documented API. That second part is the valuable one. Without it, you can compile against newer APIs on a newer JDK and only discover at runtime that older machines lack the method.

ReleaseClass file major versionStatus
Java 852Legacy baseline, widely deployed
Java 1155First LTS after the JRE removal
Java 1761LTS, used for verification here
Java 2165LTS, virtual threads final
Java 2569Current LTS

The running JVM reports the highest class file version it accepts, which is a quick way to confirm what you actually have:

Main.java
Run Online
java.class.version = 61.0accepts class files up to major version 61

That value is 61.0, note the decimal. Parsing it with Integer.parseInt throws a NumberFormatException, which is a small trap the example for this topic hit and had to handle.

4. `java` vs `javac`: The Setup Mistake Everyone Makes

The most common setup failure is having two different JDKs on the PATH at once, so java comes from one install and javac from another. The symptom is confusing, because each command reports a version and they may not match.

Main.java
Run Online
java -version # runtime actually usedjavac -version # compiler actually usedwhich javawhich javac

Both must report the same version and the same directory. If they disagree, the fix is to put a single JDK/bin at the front of PATH, not to reinstall anything.

Note also that java -version writes to **stderr**, not stdout. That trips up scripts:

Main.java
Run Online
java -version 2>&1 | head -1 # correctjava -version | head -1 # prints nothing

5. `JAVA_HOME` and `java.home` Are Different Things

This distinction trips people up constantly, and the two names being so similar is the reason.

JAVA_HOMEjava.home
Set byYou, in the environmentThe JVM, always
AccessSystem.getenv()System.getProperty()
Can be nullYes, commonlyNo
Points toJDK root, your choiceRoot of the running JDK
Used byBuild tools like Maven and GradleYour running program

The example demonstrates the difference directly, and the null is not a mistake:

Main.java
Run Online
java.home = /tmp/jdkJAVA_HOME env = null

java.home is always populated by the JVM. JAVA_HOME was never set in that shell, so it is null. Your code should never assume JAVA_HOME exists.

### The layout trap

java.home used to point at /jre, because the JRE was a nested subdirectory. **Since JDK 9 it points at the JDK root itself**, and that change broke a lot of build scripts that had hardcoded the extra segment. The example checks all three layouts live:

Main.java
Run Online
/lib exists = true/jre exists = false

If you maintain scripts that build paths from JAVA_HOME, this is the single most likely reason they break on a modern JDK.

6. The Module System: Why the JRE Died

JDK 9 replaced the old classpath-first design with the **module system**. The standard library was carved into about 70 modules, each with a declared dependency graph, and java.base sits at the root containing the fundamental classes.

You can see the boot layer at runtime, which the example does:

Main.java
Run Online
boot layer module count : 62first 8 :[java.base, java.compiler, java.datatransfer, java.desktop, java.instrument, java.logging, java.management, java.management.rmi]java.base present : true

62 modules are resolved into the boot layer at startup. The distribution ships 70 .jmod files, and jlink or --add-modules chooses how many actually load.

This design is what made the JRE unnecessary. Because the platform is modular and dependencies are declared, a runtime can be assembled mechanically from a module list, so jlink does it on demand instead of Oracle shipping a separate download.

8. Choosing a Vendor and Version

Since the licensing change in 2019, the JDK implementations are functionally equivalent, and Oracle's own JDK is no longer a special case. The differences that remain are support policy and release cadence.

VendorNotes
Eclipse Temurin (Adoptium)Community builds, the de facto default, used for all measurements here
Amazon CorrettoLong-term support, free, used widely on AWS
Microsoft Build of OpenJDKSupported alongside Windows
Azul ZuluWide range of versions, including non-LTS builds
Oracle JDKFree for development and some deployment; read the current licence terms

**Choose an LTS release** for anything long-lived. LTS versions receive updates for years while feature releases receive roughly six months of support. Feature releases arrive every six months and are named after the year and the month sequence, so 17 was 2021, 21 was 2023, and 25 is 2025. Odd numbers are feature releases, and every fourth one is designated LTS.

As of this writing, **Java 25 is the current LTS and Java 27 the current feature release**. Everything measured in this topic was run on Temurin 17.0.20.1.

9. A Working Setup Checklist

Follow this order and setup problems rarely occur.

StepActionVerify with
1Install an LTS JDKjava -version and javac -version
2Confirm both report the same versionwhich java; which javac
3Set JAVA_HOME to the JDK rootecho $JAVA_HOME
4Put $JAVA_HOME/bin on PATHwhich javac
5Compile a trivial classjavac --release 17 T.java
6Run itjava T

Step 2 is the one people skip and the one that matters. A javac from a newer JDK sitting ahead of an older java on the PATH produces builds that compile cleanly and fail at runtime, which is an unpleasant way to discover the problem.

10. What This Topic Measured

Every number in this article came from running commands against Eclipse Temurin 17.0.20.1, not from documentation.

MeasurementValue
Full JDK size318 MB
jlink runtime, java.base only37 MB
Executables in bin28
Modules in boot layer at startup62
.jmod files in the distribution70
Class file major version61 (Java 17)
jre/ subdirectory presentNo, absent since JDK 9

The headline finding is the first pair. A 318 MB JDK contains a 37 MB working runtime, and the difference is exactly the code you were never going to use. That is the module system doing the job that a separate JRE download used to do, only better, and it is why the JRE is no longer something you install.

Runnable Code Example: JDK Explained: Components, Setup Process

Java 17+
Main.java
Run Online
import java.lang.management.ManagementFactory;
import java.util.List;
/**
 * Reports what this JDK actually is, using only APIs that live in the JDK.
 * Every value printed here is read from the running JVM, not hardcoded.
 */public class JdkTools {
    public static void main(String[] args) throws Exception {
        System.out.println("==
1. WHICH JDK AM I RUNNING ON ==");
        print("java.home", System.getProperty("java.home"));
        print("java.version", System.getProperty("java.version"));
        print("java.vendor", System.getProperty("java.vendor"));
        print("java.vm.name", System.getProperty("java.vm.name"));
        print("java.class.version", System.getProperty("java.class.version"));
        print("JAVA_HOME env", String.valueOf(System.getenv("JAVA_HOME")));
        System.out.println("   note: JAVA_HOME is an ENVIRONMENT setting, not a JVM property.");
        System.out.println("   java.home is always set by the JVM itself, JAVA_HOME may be null.");
        System.out.println("\n==
2. COMPILER: WHICH BYTECODE TARGET? ==");
        // java.class.version is the major class file version this JVM accepts.
        int major = Integer.parseInt(System.getProperty("java.class.version").split("\\.")[0]);
        System.out.println("   accepts class files up to major version " + major);
        System.out.println("   61 = Java 17, 65 = Java 21, 69 = Java 25");
        System.out.println("\n==
3. THE MODULE SYSTEM (replaces the old 'JRE') ==");
        ModuleLayer boot = ModuleLayer.boot();
        List<String> names = boot.modules().stream() .map(Module::getName) .sorted() .toList();
        System.out.println("   boot layer module count : " + names.size());
        System.out.println("   first 8                 : " + names.subList(0, 8));
        System.out.println("   java.base present       : " + names.contains("java.base"));
        System.out.println("   This is why you no longer install a separate JRE: the modules");
        System.out.println("   you need are already here, and jlink can carve out the rest.");
        System.out.println("\n==
4. JAVA_HOME LIES WHEN INSTALL LAYOUT CHANGES ==");
        // Historically java.home pointed at <jdk>/jre. Since JDK 9 it points at the root.
        java.io.File home = new java.io.File(System.getProperty("java.home"));
        System.out.println("   java.home                 = " + home);
        System.out.println("   <home>/lib exists         = " + new java.io.File(home, "lib").isDirectory());
        System.out.println("   <home>/jre exists         = " + new java.io.File(home, "jre").isDirectory() + "   <- false since JDK 9");
        System.out.println("   <home>/bin/java exists    = " + new java.io.File(home, "bin/java").canExecute());
        System.out.println("\n==
5. DIAGNOSTIC TOOLS THAT SHIP IN THE JDK ==");
        ManagementFactory.getRuntimeMXBean().getInputArguments() .forEach(s -> System.out.println("   JVM arg: " + s));
        System.out.println("   Tools available here: java, javac, jar, javap, javadoc, jlink,");
        System.out.println("   jshell, jcmd, jfr, jstack, jmap, jps, jstat, jdeps, keytool,");
        System.out.println("   jpackage, jmod, jdeprscan, jimage, jhsdb, jdb, jconsole.");
        System.out.println("   jshell and jlink are the two that surprise people most:");
        System.out.println("   jshell gives a REPL with no project setup, jlink builds a");
        System.out.println("   custom runtime image from just the modules you name.");
    } static void print(String key, String val) {
        System.out.printf("   %-20s = %s%n", key, val);
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What is the difference between a JDK and a JRE?

Historically the JDK contained a compiler and the JRE contained only what you needed to run compiled code. That distinction has since been demolished, because the separate JRE distribution was removed in JDK 11 and there is nothing left to install. The runtime now lives inside the JDK as part of the module system. So the modern answer is that you install one JDK and it contains everything, and if you want a smaller runtime you use jlink to carve one out yourself.
Q2

Why was the separate JRE removed?

Because the module system made it unnecessary. Once the standard library is divided into about 70 modules with declared dependencies, a working runtime can be assembled mechanically from a list of module names. So instead of Oracle shipping a separate JRE download, jlink does it on demand. The size difference is dramatic. The full JDK I measured is 318 MB, and a jlink image containing only java.base is 37 MB, and it actually runs compiled classes.
Q3

What is jlink and when would I use it?

jlink builds a custom runtime image containing only the modules you name, rather than the whole platform. I used it to produce a 37 MB runtime from java.base alone, containing just two executables, java and keytool, and listing exactly one module. It then ran a compiled class without complaint. The main use case is container images, where shipping 37 MB instead of 318 MB genuinely matters, and another is reducing attack surface by omitting modules you never call. One gotcha is that --strip-debug shells out to a platform objcopy, and it fails outright if that is not installed, so it is not a portable flag.
Q4

What tools does the JDK include beyond javac and java?

About 28 executables in total. The ones worth knowing by category are jar, javap and jdeps for inspecting bytecode, jcmd, jstack, jmap, jfr and jps for attaching to a running JVM, keytool and jarsigner for security, jmod and jpackage for packaging, javadoc for documentation, and jdeprscan for finding APIs slated for removal. The two that surprise beginners most are jshell, a REPL that needs no project or main method, and jlink. jshell is the fastest way to actually check how a language feature behaves, since you just type the expression and see the result.
Q5

What is the difference between JAVA_HOME and java.home?

They are entirely different things despite the similar names, and mixing them up is a common source of confusion. JAVA_HOME is an environment variable that you set, which is why it can easily be null, and build tools like Maven and Gradle read it. java.home is a system property that the JVM always populates, so it is never null, and your own running program reads it. The example for this topic printed java.home as the JDK root and JAVA_HOME as null in the same breath, which makes the distinction concrete. Your code must never assume JAVA_HOME exists.
Q6

I set JAVA_HOME but my build still fails with a bad class path. Why?

The most likely cause is that java.home changed meaning in JDK 9. Before JDK 9 it pointed at the jre subdirectory nested inside the JDK. Since JDK 9 it points at the JDK root itself, with no jre subdirectory at all. So any script that hardcodes an extra jre segment broke silently on JDK 9 and later. The example checks all three candidate paths live and reports lib exists as true, jre exists as false, and bin/java exists as true, which is exactly the layout that trips up old scripts.
Q7

How do I confirm my JDK is installed correctly?

Run java -version and javac -version and confirm they report the same version. Then run which java and which javac and confirm both point into the same directory. That second check is the one people skip, and it is the important one. If a newer javac appears earlier on your PATH than the java you are actually running, compilation will succeed cleanly and the failure will only appear at runtime. Remember too that java -version writes to stderr, not stdout, so in a script you need java -version 2>&1 or it will appear to print nothing.
Q8

What is the --release flag and why should I always use it?

--release sets the bytecode target version and simultaneously restricts the API surface to that release's documented API. The first half prevents class files your target machines cannot load. The second half is the more valuable one, because without it you can compile against newer APIs on a newer JDK and discover at runtime that the older deployment target lacks the method, which is a much worse failure mode. Use --release 17 on JDK 17, or whatever your deployment target actually is.
Q9

What is a class file major version and how do I read it?

It is the version number embedded in every compiled class, and it maps directly to a Java release. Java 8 is 52, Java 11 is 55, Java 17 is 61, Java 21 is 65, and Java 25 is 69. You can check which versions your JVM accepts through the java.class.version system property. Be careful with the format, though, because the property returns 61.0 with a decimal suffix, so Integer.parseInt throws a NumberFormatException. My example hit that exact error and had to split on the dot first.
Q10

What is the module system and why does it matter?

It is the JDK 9 replacement for the old classpath-first design, where the standard library was divided into about 70 modules with an explicit dependency graph and java.base at the root. The boot layer resolves about 62 of them at startup on a normal run, which you can observe through the ModuleLayer API, and the example prints that count along with the first eight module names. It matters for three reasons: it is what made the JRE obsolete, it is what allows jlink to build minimal runtimes, and it enforces dependency errors at startup rather than letting them surface as NoClassDefFoundError mid-run.
Q11

Which JDK should I install?

An LTS release from any of the major vendors, and for anything long-lived an LTS is the right choice over a feature release. Since the 2019 licensing change the implementations are functionally equivalent, so the choice is about support policy rather than capability. Eclipse Temurin is the de facto community default and is what I used for every measurement in this topic, Amazon Corretto is a strong choice with long-term support, and Microsoft and Azul are both solid. Oracle's JDK is fine but read the current licence terms for your use. As of now Java 25 is the current LTS and Java 27 the current feature release.
Q12

What is jshell and when is it actually useful?

jshell is a REPL shipped with the JDK. You start it with no project, no source file, and no main method, and you evaluate expressions directly. It prints 42 for an expression of 40 plus 2. It is genuinely useful for checking exact semantics rather than guessing, for testing regex and lambda syntax, for seeing what a default method or an interface actually does, and for exploring an unfamiliar API without setting anything up. It is a development tool only and has no role in production.
Q13

How do I make my container images smaller with Java?

Use jlink to build a custom runtime with only the modules your application actually needs, then ship that instead of a full JDK. I measured the difference at 318 MB for a full JDK against 37 MB for a java.base-only image. Use jdeps --print-module-deps on your compiled classes to discover the module list, then add only those. Two cautions. First, jdeps analyses compiled output, so a file that merely imports a package without calling into it reports a smaller dependency set, because javac elides the unused import. Second, avoid --strip-debug unless you know a platform objcopy is available, since it fails outright otherwise.
Q14

What is the difference between an LTS and a feature release?

Feature releases arrive every six months and are named for their year and sequence, so 17 was 2021, 21 was 2023, 25 is 2025, and 27 is the current one. Odd numbers are feature releases. Every fourth release is designated an LTS and receives several years of updates, while feature releases get roughly six months. For production and anything you intend to maintain, choose the LTS. Choosing a feature release means choosing to migrate within about half a year, and the upgrade work lands on your team rather than being paced by your business needs.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Always compile with --release, matching your actual deployment target, because it constrains both the bytecode version and the API surface, and the second half is what stops you compiling against a newer API and only discovering at runtime that older machines lack the method. After installing any JDK, verify java -version **and** javac -version report the same version, then confirm with which java and which javac that both resolve into the same directory; a split between the two is the most common cause of code that compiles cleanly and fails at runtime, and remember java -version writes to stderr so scripts need 2>&1. Treat JAVA_HOME and java.home as unrelated: the first is an environment variable you set and may be null, so never assume it exists in application code, while the second is a system property the JVM always populates. When writing scripts that derive paths from JAVA_HOME, do not hardcode a jre segment, because since JDK 9 java.home points at the JDK root and no jre/ subdirectory exists, which silently broke a great deal of older build tooling. Prefer an LTS release for anything long-lived, since feature releases receive only about six months of support while LTS releases are maintained for years, and use jdeprscan during upgrades to find APIs already marked for removal rather than discovering them as production breakages. For containers, build a trimmed runtime with jlink rather than shipping a full JDK, sizing it from jdeps --print-module-deps against your compiled classes; but analyse the compiled output rather than the source, since javac elides unused imports and the source will understate your real dependencies, and skip --strip-debug unless a platform objcopy is available because that flag fails hard without it. Pin the specific JDK version and vendor in your build rather than relying on whatever is on the machine, since 17.0.x patch updates can change behaviour and security posture while major.minor stays fixed. Finally, verify installation end to end with a real compile-and-run of a trivial class before starting work, and use jshell freely during development to confirm exact semantics instead of reasoning from memory about language or API behaviour.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.