JDK vs JRE — javac Not Found in Production Builds
javac: command not found in production? JRE omits javac.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- JDK = Tools + JRE — for developers writing Java code
- JRE = JVM + standard libraries — for running Java programs
- JVM = bytecode execution engine — platform-specific, language-agnostic
- Install JDK if you compile code; install JRE-only (pre-Java9) if you only run apps
- Compiling with a newer JDK than runtime JVM causes UnsupportedClassVersionError
- Biggest mistake: thinking javac and java live in the same tool — javac comes only with JDK
JDK, JRE, and JVM are the three layers of the Java platform, and confusing them is why your production build fails with 'javac not found'. The JVM (Java Virtual Machine) is the runtime engine that executes compiled bytecode — it's the 'write once, run anywhere' magic that abstracts away the OS.
The JRE (Java Runtime Environment) bundles the JVM with core libraries and supporting files like rt.jar, giving you everything needed to run a Java application but nothing to build one. The JDK (Java Development Kit) is the superset that adds javac (the compiler), javadoc, jar, and other development tools on top of the JRE.
In production, you almost never need the JDK — you deploy a JRE or a slimmed-down JVM via a container image like Eclipse Temurin or Amazon Corretto. If you accidentally install a full JDK on your production server, you're carrying unnecessary attack surface and disk overhead; if you try to compile on a JRE-only box, you get the dreaded 'javac: command not found'.
The distinction matters because modern microservices and containerized deployments (Docker images as small as 40MB with jlink) strip the JDK entirely, leaving only the JVM modules your app actually needs.
Think of writing a letter. The JVM is the postal system — it delivers your letter anywhere in the world without you worrying about roads or planes. The JRE is the envelope, stamp, and postal rules included — everything the postal system needs to do its job. The JDK is the complete stationery kit: the envelope, stamp, AND the pen, paper, and ruler you used to write the letter in the first place. You only need the stationery kit if you're writing letters. Everyone else just needs the postal system to receive them.
Every time you install Java, you're faced with a choice: do you download the JDK or the JRE? Most beginners just pick one at random and hope for the best. That guesswork trips you up the moment something breaks — like trying to compile code and getting a 'javac not found' error — because you installed the wrong thing. Understanding the difference isn't just academic trivia; it's the foundation for every Java project you'll ever set up.
Java was built around one revolutionary promise: write your code once, run it literally anywhere — Windows, Mac, Linux, a smart fridge. That promise only holds because of a clever three-layer architecture: the JVM handles the 'run anywhere' magic, the JRE bundles everything needed to support it at runtime, and the JDK gives developers all the tools to build Java programs in the first place. Each layer has a specific job, and they nest inside each other like Russian dolls.
By the end of this article you'll know exactly what each acronym means, how the three components relate to each other, which one to install for your use case, and you'll be able to explain the whole thing confidently in a job interview. No handwaving — we're going to trace a real Java file from source code all the way to running output, so you can see every layer doing its job.
JDK vs JVM — Why javac Vanishes in Production
The JDK (Java Development Kit) is a superset of the JRE (Java Runtime Environment) that includes development tools like javac (compiler), jar, and javadoc. The JRE is the minimal runtime needed to execute compiled Java bytecode — it contains the JVM (Java Virtual Machine), core libraries, and supporting files but no compiler. The JVM is the engine that interprets or JIT-compiles bytecode into machine code at runtime. In short: JDK = JRE + dev tools; JRE = JVM + libraries; JVM = bytecode executor.
When you run java -version, you see the JVM version. When you run javac, you need the JDK. Production servers almost never need the JDK — they only need the JRE (or a minimal JDK stripped of tools). A common mistake is installing the full JDK on production containers, which bloats the image and introduces unnecessary attack surface. Conversely, a developer workstation without the JDK cannot compile code.
Use the JDK for development, CI/CD build stages, and any environment where you compile Java source files. Use the JRE (or a custom runtime image via jlink) for production deployments. This separation reduces image size by 40-60% and eliminates the risk of accidentally running javac in production — which is a sign of a broken deployment pipeline anyway.
The JVM — The Engine That Runs Your Java Code Anywhere
The Java Virtual Machine (JVM) is a program that runs on your computer and pretends to be a standardised, imaginary computer. Your Java code doesn't run directly on your CPU — it runs on this imaginary machine. That's the secret behind Java's 'write once, run anywhere' guarantee.
Here's why that matters. Windows, macOS, and Linux all have different CPUs and operating system rules. Normally, software compiled for Windows won't run on a Mac. Java sidesteps this by compiling your code into a neutral format called bytecode — instructions for the imaginary JVM machine, not for any real CPU. Every OS has its own version of the JVM, and each one knows how to translate that bytecode into whatever the local OS understands.
Think of bytecode as a universal recipe written in a language every chef (JVM) speaks, even though the actual kitchen equipment (CPU/OS) differs from country to country. The JVM is also responsible for memory management — it automatically cleans up objects your program no longer needs, through a process called garbage collection. You don't free memory manually in Java; the JVM handles it.
Critically, the JVM only understands bytecode. It cannot read your original .java source file. Something has to compile that source file first — and that's where the other two components come in.
// This file is your SOURCE CODE — written by a human, readable by humans. // The JVM cannot run this directly. It must be compiled to bytecode first. public class HelloJVM { public static void main(String[] args) { // This message proves our code reached the JVM and executed. System.out.println("Hello from the JVM!"); // The JVM knows which operating system it is running on. // We can ask it at runtime — notice we never wrote OS-specific code. String operatingSystem = System.getProperty("os.name"); System.out.println("Running on: " + operatingSystem); // The JVM also tells us the Java version it is using. String javaVersion = System.getProperty("java.version"); System.out.println("Java version: " + javaVersion); } }
The JRE — The Complete Runtime Package Your Program Needs to Run
The Java Runtime Environment (JRE) is the JVM plus a large library of pre-written code that Java programs rely on at runtime. You can think of the JRE as the JVM bundled with its toolbox.
When your Java program says System.out.println(...), it's not magic — System, out, and println are all classes and methods written by the Java team, pre-compiled, and shipped as part of the JRE. This collection of pre-written classes is called the Java Class Library (or the Java API). It contains thousands of ready-made tools: ways to read files, connect to the internet, format dates, sort lists, and much more.
Without the JRE, the JVM would be like a car engine with no wheels, seats, or fuel system — technically impressive but not going anywhere.
Before Java 9, you could install the JRE standalone — perfect for end users who just wanted to run a Java app, not build one. From Java 9 onwards, Oracle merged the JRE into the JDK for distribution purposes. But the conceptual distinction still exists and still matters, especially when you're creating a minimal production deployment using the jlink tool, which lets you bundle only the JVM + the specific library modules your application actually uses, making your deployment tiny and fast.
// This example uses classes from the Java Class Library — the part of the JRE // that isn't the JVM itself. We didn't write ArrayList, LocalDate, or Collections. // They came pre-packaged in the JRE. The JVM executes them just like our own code. import java.util.ArrayList; // From the JRE's java.util library import java.util.Collections; // Also from java.util import java.time.LocalDate; // From the JRE's java.time library (added in Java 8) public class JRELibraryDemo { public static void main(String[] args) { // ArrayList is a resizable list — provided by the JRE, not written by us. ArrayList<String> programmingLanguages = new ArrayList<>(); programmingLanguages.add("Java"); programmingLanguages.add("Python"); programmingLanguages.add("Rust"); programmingLanguages.add("Go"); // Collections.sort() is also a JRE utility — alphabetically sorts our list. Collections.sort(programmingLanguages); System.out.println("Sorted languages: " + programmingLanguages); // LocalDate.now() asks the JRE to fetch today's date using the OS clock. LocalDate today = LocalDate.now(); System.out.println("Today's date from JRE: " + today); // We can also demonstrate the JRE's string formatting utilities. String message = String.format("There are %d languages in the list.", programmingLanguages.size()); System.out.println(message); } }
java. or javax. is part of the Java Class Library shipped with the JRE. You're using the JRE's toolbox every single time you write import java.util.ArrayList — you just never had to download or install it separately because it came bundled with your JDK.The JDK — The Full Developer Toolkit That Contains Everything
The Java Development Kit (JDK) is the complete package. It contains the JRE (which contains the JVM), plus a set of developer tools you need to actually build Java programs. The most important tool is javac — the Java compiler. It's the program that reads your .java source file and outputs a .class bytecode file that the JVM can then execute.
Other tools bundled in the JDK include: javadoc (generates HTML documentation from your code comments), jar (packages your compiled classes into a single distributable archive file), jdb (a command-line debugger), jshell (an interactive console introduced in Java 9, great for experimenting), and jlink (packages a minimal JRE for your specific app).
The relationship is simple nesting: JDK ⊃ JRE ⊃ JVM. The JDK is the outermost layer. It contains everything. If you're writing Java code — which you are, since you're reading this — install the JDK. Full stop.
When you run javac HelloJVM.java in your terminal, you're using a JDK tool. When you then run java HelloJVM, you're using the JVM inside the JRE inside the JDK. Two different tools in the same box, each doing a distinct job in the pipeline from source code to running program.
// STEP 1 — You write this source code and save it as CompilationPipelineDemo.java // STEP 2 — You run: javac CompilationPipelineDemo.java (JDK tool: the compiler) // This produces: CompilationPipelineDemo.class (bytecode — not human-readable) // STEP 3 — You run: java CompilationPipelineDemo (JVM inside JRE inside JDK) // The JVM loads the .class file and executes the bytecode. public class CompilationPipelineDemo { public static void main(String[] args) { // This line only runs because ALL THREE layers did their job: // JDK compiler turned our .java into .class bytecode. // JRE provided the System class and its out.println method. // JVM executed the bytecode instruction that calls println. System.out.println("All three layers (JDK, JRE, JVM) worked together to print this!"); // We can prove the JDK compiled this by checking the class file version. // The number maps to a Java version: 61 = Java 17, 55 = Java 11, 52 = Java 8. int classMajorVersion = CompilationPipelineDemo.class.getPackage() == null ? 0 : 0; // Package check placeholder // A more reliable way — ask the JVM what version compiled this class. System.out.println("Class file compiled with Java spec version: " + System.getProperty("java.class.version")); System.out.println("JDK version used to compile: " + System.getProperty("java.version")); System.out.println("JVM vendor running this code: " + System.getProperty("java.vendor")); } }
.class file with a JVM from Java 8, they'll get an UnsupportedClassVersionError. Always know your target runtime version. Use javac --release 11 MyFile.java to compile for a specific older version.How JDK, JRE, and JVM Work Together — Tracing One Java Program End to End
Let's tie it all together by following a single Java file through its complete lifecycle. This is the story you should be able to tell from memory.
You open your editor and write Greeting.java. At this point nothing has happened yet — it's just a text file. You run javac Greeting.java. The javac compiler (a JDK tool) reads your source code, checks it for syntax errors, and if everything's fine, produces Greeting.class. This .class file contains bytecode — compact, platform-neutral instructions that no human CPU understands natively, but every JVM does.
Now you run java Greeting. The java launcher (part of the JRE) starts the JVM and hands it your .class file. The JVM's class loader finds Greeting.class and loads it into memory. The JVM then runs a Just-In-Time (JIT) compiler that translates the bytecode into native machine code for your specific CPU — this happens at runtime, which is why Java 'warms up' over time and gets faster the longer it runs. Finally, your main method executes, the JRE's System.out.println does its job, and you see output in the terminal.
Three layers. One seamless pipeline. Understanding this flow means you can diagnose almost any Java setup problem on your own.
// Full end-to-end example — write this, compile it, run it, and trace every layer. // // COMPILE: javac Greeting.java <-- JDK's javac tool is used here // RUN: java Greeting <-- JVM (inside JRE inside JDK) takes over here public class Greeting { // The JVM always looks for a method with EXACTLY this signature as the entry point. // 'public' — callable from outside. 'static' — no object needed to call it. // 'void' — returns nothing. 'String[] args' — command-line arguments passed in. public static void main(String[] args) { String recipientName = "Java Developer"; // System — a class provided by the JRE's java.lang library (auto-imported) // .out — a static field on System; it's a PrintStream object // .println() — a method on PrintStream that writes a line to the console System.out.println("Hello, " + recipientName + "!"); // Demonstrating that the JVM manages memory for us. // We create an object — the JVM's garbage collector will clean it up // automatically when this method ends and the reference goes out of scope. StringBuilder messageBuilder = new StringBuilder(); messageBuilder.append("JDK compiled me. "); messageBuilder.append("JRE provided StringBuilder. "); messageBuilder.append("JVM is running me right now."); System.out.println(messageBuilder.toString()); // When main() returns, the JVM will shut down cleanly. // No manual memory cleanup needed — that's garbage collection at work. } }
Choosing Between JDK and JRE: A Production Decision Guide
Now that you understand the layers, here's the practical decision framework for every environment:
- Dev machines: Always install JDK. You need javac, jshell, jar, and other tools. The JDK includes the JRE, so you get everything.
- Production servers running your app: Use a minimal JRE (or a custom runtime image built with jlink). This reduces attack surface and image size. Many teams use
eclipse-temurin:17-jre-alpineas their base. - CI/CD build agents: JDK required for compilation. Make sure the version matches your target environment.
- Containerization: Use multi-stage builds. Stage 1: JDK to compile. Stage 2: JRE to run. This slashes image size by 60-80%.
- End users who just run a desktop app: Historically JRE, but nowadays you bundle a runtime with the app (using jlink or packaging tools like jpackage).
Java 9+ removed standalone JRE distributions from Oracle, but the concept persists. Vendors like Adoptium still offer JRE-only builds. Always read the distribution page carefully — if it says 'JDK' it includes everything; if it says 'JRE' you cannot compile.
There's one more nuance: when you run java from a JDK installation, you're using the JVM that lives inside the JRE that lives inside the JDK. But when you run javac, you're using a tool that only exists at the JDK level. This is why java -version succeeds on both JRE and JDK, but javac -version fails on JRE-only.
#!/bin/bash # Check if you have JDK or JRE by testing javac echo "Checking Java environment..." if command -v javac &> /dev/null then echo "JDK detected: javac is available" javac -version else echo "JRE detected: javac not found" echo "Install JDK if you need to compile code." fi echo "" echo "Java version:" java -version
- JVM: the drill — platform-specific, runs bytecode.
- JRE: drill + bits and bits — JVM + standard libraries.
- JDK: drill + bits + screwdrivers & wrenches — JRE + javac, jar, javadoc, etc.
- You don't need the whole workshop to use the drill once it's assembled.
- jlink lets you create a custom toolbelt with only the bits your project uses.
Why the JVM's Memory Model Burns Junior Devs in Production
You don't need to know JVM architecture to ship a Spring Boot app. You need to know it when your Pod crashes with OOMKilled at 2 AM. The JVM divides memory into regions: Heap (where objects live), Stack (where method calls and primitives live), Metaspace (class metadata), and Code Cache (JIT'd native code). The garbage collector runs in the Heap. If you don't set -Xmx and -Xms, the JVM defaults to 1/4 of your physical RAM. In a container with 512 MB limit? That's 128 MB heap. Your Spring Boot app with HikariCP and 30 beans? Dead on arrival. The real pain: most devs treat the JVM as a black box. Set identical heap flags in dev and prod. Then watch the GC logs. Your code is not the problem. The collector configuration is.
// io.thecodeforge.jvm-memory-setup FROM eclipse-temurin:21-jdk-alpine # Never trust defaults. Explicit heap + GC strategy. ENV JAVA_OPTS="-Xms256m -Xmx256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -XX:+PrintGCDateStamps" COPY target/order-service-*.jar app.jar ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
How the JDK's 'javac' Betrays You When You Assume It Exists in Prod
The JDK ships javac, the Java compiler. In production, you never compile. You run bytecode. So why does your Docker image still pull the full JDK? It adds 200 MB+ to your image. Worse: if someone accidentally runs javac in the container, it fails silently—or worse, compiles a malicious .java file from a mounted volume. The fix: use a JRE or a jlink custom runtime. Spring Boot 3.x ships as an executable JAR with embedded Tomcat. It needs only the JVM and the class libraries. Strip the rest. jlink --add-modules java.base,java.sql,java.naming,java.management --output /custom-jre. That's a 40 MB runtime. No compiler. No debug tools. No attack surface. Your production image should be lean enough that you can explain every layer. The JDK in prod is a lazy choice, not a safe one.
// io.thecodeforge.jlink-custom-runtime // 1. Generate minimal JRE for Spring Boot: // jlink --add-modules java.base,java.sql,java.naming,java.management,java.instrument,jdk.unsupported \ // --output /custom-jre --strip-debug --no-man-pages --no-header-files // // 2. Use in Docker: // FROM alpine:3.19 // COPY --from=build /custom-jre /opt/java // ENV PATH=/opt/java/bin:$PATH // COPY target/app-*.jar /app.jar // CMD ["java", "-jar", "/app.jar"] public class VerifyRuntime { public static void main(String[] args) { // This fails if javac isn't present—which is what we want. System.out.println("JVM version: " + System.getProperty("java.version")); // Try: Runtime.getRuntime().exec("javac"); → throws IOException } }
The 'javac Not Found' Production Panic
- Always distinguish build-time images (need JDK) from runtime images (JRE or JRE-slim).
- Use multi-stage Docker builds: JDK for compilation, JRE for running.
- Verify installed Java components with 'java -version' vs 'javac -version' on every environment.
java -versionjavac -versionjavap -verbose MyClass.class | grep 'major version'java -version (note the version number)which javals -la $(which java) (check symlinks)echo $JAVA_HOMEls $JAVA_HOME/bin/java| Feature / Aspect | JVM | JRE | JDK |
|---|---|---|---|
| Full name | Java Virtual Machine | Java Runtime Environment | Java Development Kit |
| Primary job | Execute bytecode on your specific OS/CPU | Provide the runtime — JVM + standard libraries | Provide everything for building Java programs |
| Contains | Bytecode interpreter + JIT compiler + GC | JVM + Java Class Library (java.util, java.io, etc.) | JRE + javac + javadoc + jar + jdb + jshell + jlink |
| File it works with | .class bytecode files only | .class bytecode files + library .jar files | .java source files (compiles them to .class) |
| Who needs it | Everyone running Java | Anyone running a Java app (end users, servers) | Developers writing Java code |
| Can compile .java files? | No | No | Yes — via the javac command |
| Can run .class files? | Yes — that's its whole job | Yes — via the java launcher command | Yes — JRE is included inside the JDK |
| Installed standalone? | Bundled inside JRE, not installed alone | Pre-Java 9 yes; post-Java 9 merged into JDK | Yes — single download covers all three |
| Platform-specific? | Yes — different JVM per OS/CPU | Yes — ships with the OS-specific JVM | Yes — download the JDK for your specific OS |
| Typical size | ~50 MB (core JVM only) | ~100–200 MB (JVM + libraries) | ~200–400 MB (everything) |
| Example vendor builds | HotSpot, OpenJ9, GraalVM (JVM only mode) | Adoptium JRE, Oracle JRE, Amazon Corretto JRE | Adoptium JDK, Oracle JDK, Amazon Corretto JDK, GraalVM JDK |
| File | Command / Code | Purpose |
|---|---|---|
| HelloJVM.java | public class HelloJVM { | The JVM |
| JRELibraryDemo.java | public class JRELibraryDemo { | The JRE |
| CompilationPipelineDemo.java | public class CompilationPipelineDemo { | The JDK |
| Greeting.java | public class Greeting { | How JDK, JRE, and JVM Work Together |
| check_version.sh | echo "Checking Java environment..." | Choosing Between JDK and JRE |
| Dockerfile | FROM eclipse-temurin:21-jdk-alpine | Why the JVM's Memory Model Burns Junior Devs in Production |
| ModuleInfo.java | public class VerifyRuntime { | How the JDK's 'javac' Betrays You When You Assume It Exists |
Key takeaways
import java.* you write pulls from the library the JRE provides.javac --release <version> to control bytecode compatibility. Never assume the runtime JVM matches your build JDK.Common mistakes to avoid
5 patternsInstalling only JRE and then trying to compile code
javac HelloWorld.java gives 'javac' is not recognized (Windows) or command not found: javac (Mac/Linux).Compiling with a newer JDK than the JVM version on the target machine
java.lang.UnsupportedClassVersionError: Unsupported major.minor version 61.0 when deployed to a server.javac --release 11 MyApp.java. This tells the JDK to produce bytecode compatible with Java 11 even if your local JDK is version 17.Confusing the JVM with the Java language itself
.class bytecode format that Java's compiler produces. The JVM runs all of it without knowing or caring which language was used. Java the language and the JVM are separate things that happen to be developed together.Not using multi-stage Docker builds — deploying with JDK in production
Assuming `java` and `javac` are always at the same version
$JAVA_HOME/bin to PATH. Verify both java -version and javac -version report the same version.Interview Questions on This Topic
Can you explain the relationship between the JDK, JRE, and JVM? Which one contains which?
If a user just wants to run a Java application on their machine but has no interest in coding, what should they install and why?
What is bytecode, and why does its existence mean Java programs can run on any operating system without recompiling?
Explain the difference between the command `java` and `javac`. Which component provides each?
javac is the Java compiler, a development tool that reads .java source files and produces .class bytecode files. It is provided exclusively by the JDK. java is the launcher that starts the JVM and runs compiled .class files. It is part of the JRE (and hence also available in the JDK). If you have only the JRE installed, you can run Java programs with java, but you'll get a 'command not found' error for javac because the compiler is not included.What is the `--release` flag in javac, and when would you use it in production?
--release flag tells the compiler to produce bytecode compatible with a specific Java version, regardless of the JDK version you're using to compile. For example, javac --release 11 MyApp.java will produce bytecode that runs on any Java 11 or later JVM, even if you're using JDK 17. This is critical in production when your build environment may have a newer JDK than your target runtime. Without it, you risk UnsupportedClassVersionError when deploying to older JVMs. It's a best practice to always specify --release matching your target environment in build scripts.Frequently Asked Questions
No. The JDK already includes the JRE inside it. If you're a developer writing Java code, just install the JDK and you're covered. Installing both separately is redundant and can sometimes cause version conflicts if they're different versions.
Bytecode is the intermediate format that javac compiles your .java source code into. It's stored in .class files and is not specific to any CPU or operating system. The JVM on any platform reads this bytecode and translates it into the native instructions for that machine. This is the mechanism that makes Java's 'write once, run anywhere' promise real.
Conceptually, yes — absolutely. The JRE as a concept (JVM + standard libraries) still exists inside every JDK; Oracle just stopped releasing it as a separate standalone download. The distinction matters when you use tools like jlink to build a custom minimal runtime for production deployments, or when you're reading older documentation and job descriptions. You'll still see JRE mentioned constantly in the industry.
Not directly. You need at least a JRE to run Java programs. However, some tools like jlink or packaging tools (jpackage, launch4j) can bundle a minimal JRE with your application, so the end user doesn't need to install Java separately. Many desktop Java applications distribute this way.
OpenJDK is the open-source reference implementation of the Java SE specification. Oracle JDK is built from OpenJDK but includes some additional commercial features (like Flight Recorder in older versions) and is supported by Oracle. Since Java 11, both are essentially identical in functionality, with Oracle JDK offering a different support model. For most purposes, OpenJDK builds (like Adoptium Temurin) are free and production-ready.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Java Basics. Mark it forged?
7 min read · try the examples if you haven't