How Java runs: javac, bytecode and the JVM
What javac produces, what the JVM does with it, the difference between JDK, JRE and JVM, and how to answer platform independence without a slogan.
The first question in a Java technical round is almost always some version of "why is Java called platform independent?" — and the answer most students give is a slogan they cannot unpack. Let us fix that first, because everything else in the language makes more sense afterwards.
The two steps
public class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}$ javac Hello.java → produces Hello.class (bytecode)
$ java Hello → the JVM runs Hello.classjavac compiles your source to bytecode, not to machine code. Hello.class contains
instructions for a machine that does not exist in silicon — the Java Virtual Machine. You can read
them:
$ javap -c Hello
public static void main(java.lang.String[]);
0: getstatic #7 // Field java/lang/System.out
3: ldc #13 // String Hello
5: invokevirtual #15 // Method java/io/PrintStream.println
8: returnjava starts a JVM, which executes that bytecode. The JVM is a real program compiled for
your operating system — a different JVM binary for Windows, Linux and macOS, all of which
understand the same .class file.
So the answer to "why platform independent?"
Java source compiles to bytecode, which is not specific to any CPU or operating system. Each platform has its own JVM, and every JVM understands the same bytecode. So the compiled artefact is portable — write once, compile once, run anywhere there is a JVM.
Then add the honest second half, because it is what separates a memorised answer from an understood one:
The bytecode is platform independent. The JVM is not — it is platform dependent, deliberately. The portability comes from having a platform-specific interpreter for a platform-neutral format.
That second sentence is the one interviewers are listening for.
JDK, JRE and JVM
Three names, frequently confused, and a very common one-mark question.
| What it is | Contains | |
|---|---|---|
| JVM | The engine that executes bytecode | Class loader, bytecode verifier, interpreter, JIT, garbage collector |
| JRE | Everything needed to run a Java program | JVM + the standard class libraries |
| JDK | Everything needed to develop one | JRE + javac, javap, jar, jdb and friends |
So: JDK ⊃ JRE ⊃ JVM. You need a JDK to compile, and only a JRE to run — though since Java 11 the standalone JRE is no longer shipped separately, and you simply install a JDK.
The JIT, and why Java is not slow
If bytecode were only ever interpreted, Java would be roughly as slow as a scripting language. It is not, because of the JIT (Just-In-Time) compiler inside the JVM.
The JVM starts by interpreting. While it does, it counts how often each method runs. A method that runs often enough — a "hot" method — is compiled to native machine code and the native version is used from then on. Because the JIT is watching the program actually run, it can optimise using information a static compiler never has: which branch is usually taken, which types actually turn up at a call site.
This gives Java its characteristic performance shape: slower to start, fast once warm. It is also why a naive benchmark that runs a loop once measures the interpreter, not the JIT.
What the JVM does when you run a class
Worth knowing in outline, because "explain class loading" is asked in slightly better interviews:
- Loading — the class loader finds
Hello.classand reads it into memory. There is a hierarchy of loaders (bootstrap → platform → application), and a child asks its parent first. - Verification — the bytecode verifier checks the bytecode is well-formed and type-safe before anything runs. This is a real security boundary: you cannot hand the JVM bytecode that reads arbitrary memory.
- Preparation — static fields are allocated and set to their default values (
0,null,false). - Initialisation — static initialisers and static field assignments run, once, in order.
- Execution —
mainis invoked.
Note step 4: static initialisation is lazy. A class is initialised the first time it is actually used, not when the program starts.
Memory, in one diagram
┌─ JVM ─────────────────────────────────────────────┐
│ Heap all objects, shared, garbage collected│
│ Method area class metadata, static fields │
│ Stack one per thread — frames, locals, │
│ references. Overflows on deep recursion│
│ PC register per thread, current instruction │
└────────────────────────────────────────────────────┘The one distinction to hold on to: objects live on the heap, references and primitives live on the stack. So this:
int x = 5; // the value 5 is on the stack
String s = new String("hi"); // s is a reference on the stack,
// the String object is on the heapStackOverflowError comes from too many stack frames — usually runaway recursion.
OutOfMemoryError comes from a full heap. Two different failures, two different causes, and being
able to separate them is worth a mark.
The rules main has to obey
public static void main(String[] args)Each word is there for a reason, and this is a standard question:
public— the JVM must be able to call it from outside the class.static— it is called before any object exists, so it cannot need an instance.void— nothing meaningful could consume a return value; the exit code comes fromSystem.exit.String[] args— the command-line arguments.String... argsalso works, since varargs are arrays underneath.
The parameter name is yours to choose. The rest is fixed. And a file may contain several classes,
but only one public class, whose name must match the file name.
Java compared with C++ and Python
A comparison question you can prepare for once and reuse:
- Versus C++: no pointer arithmetic, no manual
delete(the garbage collector owns reclamation), no multiple inheritance of classes, and compilation targets a VM rather than the CPU. Java trades control for safety. - Versus Python: statically typed and checked at compile time rather than at run time, so a type error is caught before the program starts. Much more verbose, and considerably faster once the JIT has warmed up.
What to take into an interview
javacproduces bytecode; the JVM executes it. Bytecode is portable; the JVM is not.- JDK ⊃ JRE ⊃ JVM — develop, run, execute.
- The JIT compiles hot methods to native code at run time, which is why Java starts slow and runs fast.
- Class loading is load → verify → prepare → initialise → execute, and it is lazy.
- Objects are on the heap; references and primitives are on the stack.
StackOverflowErroris deep recursion;OutOfMemoryErroris a full heap.mainmust bepublic static voidand take aString[], and each of those words has a reason.