Lesson 01 · 9 min read · Java for placements

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.class

javac 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: return

java 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:

  1. Loading — the class loader finds Hello.class and reads it into memory. There is a hierarchy of loaders (bootstrap → platform → application), and a child asks its parent first.
  2. 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.
  3. Preparation — static fields are allocated and set to their default values (0, null, false).
  4. Initialisation — static initialisers and static field assignments run, once, in order.
  5. Executionmain is 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 heap

StackOverflowError 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 from System.exit.
  • String[] args — the command-line arguments. String... args also 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

  • javac produces 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.
  • StackOverflowError is deep recursion; OutOfMemoryError is a full heap.
  • main must be public static void and take a String[], and each of those words has a reason.
Lesson 1 of 12 · all 12 are free to read, no account. Course contents