Java Compilation & Execution
Java source goes through a distinct compile step (javac, producing bytecode) and a separate run step (java, loading and executing that bytecode on the JVM).
Unlike a purely interpreted language, Java has a clear two-stage life cycle: source code (.java) is compiled ahead of time into bytecode (.class), and that bytecode is later loaded and executed by a Java Virtual Machine. This topic traces that pipeline from source file to running program, including the tools involved (javac, java), classpath basics, and common command-line flags — without diving into full JVM internals, which are their own later topic.
The Overall Pipeline: Source → Bytecode → JVM Execution
source code (text) bytecode (binary) class loading, verification, interpretation/JIT, execution javac performs a one-time ahead-of-time compilation to bytecode; the JVM performs class loading and execution every time the program runs.
javac: The Java Compiler
javac reads one or more .java source files, performs full compile-time checks (syntax, type checking, resolving imports), and emits one .class file per class/interface/enum/record it finds — including nested types, each getting its own file such as Outer$Inner.class.
$ javac HelloWorld.java
$ ls
HelloWorld.class HelloWorld.java.class Files and Bytecode
A .class file holds bytecode: a compact, platform-neutral set of instructions for a stack-based virtual machine, along with a constant pool (literals, symbolic references) and metadata about the class (its fields, methods, superclass, implemented interfaces). Bytecode is not native machine code — it is designed to be interpreted or JIT-compiled by a JVM, which is what makes it portable across operating systems and CPU architectures.
The java Launcher and Class Loading
The java command starts a new JVM instance, which locates the requested class (by searching the classpath), loads its bytecode into memory, performs bytecode verification, and then begins execution at that class's main method.
$ java HelloWorld
Hello, World!Bytecode Verification (High Level)
Before running loaded bytecode, the JVM's verifier checks that it is structurally valid and does not violate the language's safety rules — for example, that it does not perform illegal type casts, does not overflow the operand stack, and does not jump to invalid instructions. This is a key part of Java's security model: it means the JVM does not have to blindly trust that bytecode (which could come from an untrusted source, historically applets) is well-formed.
Verification details and exact algorithms are JVM-implementation behavior — the JLS requires that unsafe bytecode be rejected, but the specific verification technique used is not part of the language specification.
Interpretation and JIT Compilation (High Level)
A typical JVM begins by interpreting bytecode instruction by instruction. As it runs, it profiles which methods are executed frequently ('hot' code) and just-in-time (JIT) compiles those methods into optimized native machine code, replacing the slower interpreted path. This hybrid approach is why long-running Java programs often speed up after a brief warm-up period.
The exact interpretation/JIT strategy (tiered compilation, specific optimization heuristics) is JVM-implementation-dependent and not mandated by the Java Language Specification — different JVMs (HotSpot, GraalVM, etc.) may behave differently here.
Compile Errors Versus Runtime Errors
A compile error is caught by javac before any .class file is produced — a syntax mistake, a type mismatch, an undeclared variable. The program never runs. A runtime error occurs while the compiled program is executing — a NullPointerException, an ArrayIndexOutOfBoundsException, a failed file read — the program compiled fine but hit a problem during execution.
int x = "oops"; // compile error: incompatible types, caught by javac
int[] arr = new int[3];
System.out.println(arr[5]); // compiles fine; throws ArrayIndexOutOfBoundsException at runtimeClasspath Basics
The classpath is the set of locations (directories and/or JAR files) the JVM (and javac, for resolving referenced classes) searches to find compiled classes by name. If a class cannot be found on the classpath, the JVM throws a NoClassDefFoundError or ClassNotFoundException at run time.
-d, -cp / --class-path, and Version Flags
Common javac and java flags used when compiling and running multi-file or packaged programs:
javac -d <dir> Src.java — write generated .class files into <dir> (respecting package structure). • java -cp <path> ClassName or java --class-path <path> ClassName — specify where to look for compiled classes. • java -version / javac -version — print the installed version. • java --release 17 (or javac --release 17) — compile/run against a specific Java language and API version.
The -d out flag places App.class under out/com/example/App.class, mirroring the package declaration; -cp out tells the launcher to look there.
$ javac -d out src/com/example/App.java
$ java -cp out com.example.AppJAR Overview
A JAR (Java ARchive) file bundles many .class files (and resources) into a single zip-format archive for easier distribution, optionally with a manifest specifying, among other things, a main class to run directly. JAR files are covered in full in their own topic; here it's enough to know they exist as a packaging mechanism sitting on top of ordinary .class files.
$ jar cfe app.jar com.example.App -C out .
$ java -jar app.jarKey Takeaways
javac compiles .java source into platform-neutral .class bytecode; java loads and executes that bytecode.
Compile errors are caught by javac before the program runs; runtime errors occur during execution of already-compiled code.
The JVM verifies bytecode for safety before executing it, then typically interprets it and JIT-compiles hot code paths — exact strategy is JVM-implementation-dependent.
The classpath tells the compiler and JVM where to find compiled classes; -cp/--class-path and -d are the core flags for controlling it.
JAR files package many .class files and resources into one distributable archive.
Assuming javac errors and runtime exceptions are the same category of problem.
Treat compile errors (caught by javac, program never runs) and runtime exceptions (thrown while a successfully compiled program executes) as distinct categories with different causes.
Conflating the two leads to debugging in the wrong place — e.g. trying to 'catch' a compile error with a try/catch block, which is meaningless since the program never started.
The java launcher takes a fully qualified class name, not a file name — passing the .class extension is a common mistake and causes an error locating the class.
What are the two main command-line tools in the basic Java compile/run pipeline, and what does each do?
javac compiles .java source files into .class bytecode files; java starts a JVM that loads and executes a compiled class's bytecode.
Why is bytecode described as 'platform-neutral'?
Bytecode is not native machine code for any specific CPU/OS — it is an intermediate instruction format that any conforming JVM, regardless of platform, can load, verify, and execute.
Give an example of a compile error and an example of a runtime error.
Compile error: assigning a String to an int variable (caught by javac). Runtime error: accessing an array index out of bounds, which compiles fine but throws ArrayIndexOutOfBoundsException when executed.
What does the -cp (or --class-path) flag control?
It tells javac or java where (which directories/JARs) to search for compiled classes referenced by the program.
What is bytecode verification, and why does the JVM perform it?
It is a check the JVM performs on loaded bytecode before execution, confirming it is structurally valid and doesn't violate Java's safety rules (e.g. illegal casts, stack overflows) — important because bytecode may come from untrusted sources.
What is JIT compilation, at a high level, and why does a long-running Java program sometimes get faster after it starts?
JIT (just-in-time) compilation translates frequently executed ('hot') bytecode into optimized native machine code at run time. Programs often start out interpreted and speed up once hot methods are JIT-compiled — this warm-up behavior is JVM-implementation-dependent.
What is a JAR file's basic purpose?
It bundles many .class files and resources into a single distributable archive, optionally naming a main class to run directly with java -jar.
What is wrong with the second command, and what should it be?
java takes a class name, not a file name — it should be java Foo (no .class extension). Running java Foo.class typically fails because the launcher looks for a class literally named 'Foo.class', which doesn't exist.
Write the two shell commands needed to compile a file com/example/App.java into an output directory named build, and then run it, given its package is com.example.
javac -d build com/example/App.java java -cp build com.example.App -d build places App.class at build/com/example/App.class per the package structure; -cp build tells java where to search, and the class is referred to by its fully qualified name com.example.App.
Does this line cause a compile error or a runtime error, and why?
A runtime error — integer division by zero compiles fine (the compiler doesn't statically prove the divisor is zero in general) but throws an ArithmeticException when executed.
$ javac Foo.java
$ java Foo.classint total = 10 / 0;