What Happens When a Java Program Runs

Naveen AnamaneniUpdated · 6 min read

You typed two commands, javac and java, and a line of text appeared. A lot happened in between. Knowing the main steps helps you understand error messages: some mistakes are caught when you compile, others only when the program runs, and the step tells you where to look.

Two Stages: Compile Time and Runtime

Every Java program goes through two separate stages.

From Fare.java to running code: compile time, then runtime
StageStarted byWhat happensMistakes found here
Compile timejavac Fare.javaYour code is checked and translated into bytecode in Fare.classTyping and syntax mistakes: a missing ;, a wrong type, a misspelled name
Runtimejava FareThe JVM loads the bytecode and runs itProblems that depend on running: dividing by zero, a missing file, reading past the end of an array

At Compile Time: What javac Does

When you run javac Fare.java, the compiler:

  1. Reads your source code and splits it into words and symbols.
  2. Checks the grammar: braces match, statements end with ;, keywords are spelt right.
  3. Checks the meaning: every variable exists, every value has the right type, every method you call is real.
  4. Writes bytecode to Fare.class: compact instructions for the JVM, not for any one processor.

If any check fails, javac prints an error with the file name and line number, and stops.

At Runtime: What the JVM Does

When you run java Fare, the java launcher starts a JVM, and the JVM:

  1. Loads the class. The class loader finds Fare.class and reads it into memory. Classes you use later, such as String or ArrayList, are loaded when they are first needed.
  2. Verifies the bytecode. The bytecode verifier checks that the instructions are safe: no jumping into the middle of nowhere, no treating a number as an object. This protects the JVM from damaged or tampered class files.
  3. Finds main and runs it. The execution engine starts by interpreting the bytecode, one instruction at a time.
  4. Compiles hot code. The JIT (just-in-time) compiler watches which methods and loops run most often and turns them into fast machine code for your processor.
  5. Cleans up memory. While the program runs, the garbage collector frees objects that are no longer used.

See It Step by Step

This program does a little work so there's something for each stage to do.

Fare.java▶ Try it
public class Fare {
    public static void main(String[] args) {
        int farePerPerson = 450;
        int passengers = 3;
        int total = farePerPerson * passengers;
        System.out.println(total);
    }
}
Output
1350

How it works

  1. At compile time, javac checks that farePerPerson, passengers and total are declared as int before they're used, and that * makes sense for two int values.
  2. It writes Fare.class with bytecode for the multiplication and the print.
  3. At runtime, the class loader loads Fare, the verifier checks it, and the interpreter runs main.
  4. Line 6 needs System and PrintStream, so those classes are loaded too, and 1350 is printed.

Try it yourself: change line 4 to int passengers = "three";. Does the mistake appear when you compile, or when you run?

Common Mistakes

Thinking javac runs the program. javac only checks and translates. Nothing runs until you use java.

Thinking the JVM runs your .java file. The JVM only understands bytecode. Even java Fare.java compiles the file in memory first, then runs the bytecode.

Expecting every error at compile time. The compiler can't know that a file will be missing or that a user will type a letter instead of a number. Those problems show up as exceptions at runtime, which a later lesson teaches you to handle.

Under the Hood

You can look at the bytecode yourself with the javap tool from the JDK. Compile first, then disassemble:

Terminal
javac Fare.java
javap -c Fare

Part of the output looks like this (here for a version of Fare that multiplies 450 * 3 directly):

javap output
public static void main(java.lang.String[]);
  Code:
     0: sipush        1350
     3: istore_1
     4: getstatic     #7    // Field java/lang/System.out
     7: iload_1
     8: invokevirtual #13   // Method java/io/PrintStream.println:(I)V
    11: return

Two things to notice:

  • javac already worked out 450 * 3 = 1350 at compile time, because both numbers were fixed. This is called constant folding.
  • The instructions are simple: push a number, store it, load it, call println. That's the language the JVM understands. A later lesson on bytecode goes into detail.

Interview Corner

What happens when you compile and run a Java program?

javac checks the source code and compiles it into bytecode in a .class file. When you run it with java, the JVM's class loader loads the class, the bytecode verifier checks it, and the execution engine runs it, interpreting at first and using the JIT compiler to turn frequently used code into machine code. The garbage collector frees unused memory while it runs.

What is the difference between a compile-time error and a runtime error?

A compile-time error is found by javac before the program runs, such as a missing semicolon or a type mismatch, and no .class file is produced. A runtime error happens while the program runs, such as dividing by zero, and usually appears as an exception.

Practice

  1. Easy: For each mistake, say whether it is found at compile time or at runtime: a missing ;, dividing a number by zero, a misspelled variable name, opening a file that doesn't exist.
  2. Harder: Compile Fare.java and run javap -c Fare. Find the instruction that multiplies the two numbers. Hint: look for imul. Why didn't the folded version above have it?

Quick check: Program internals

4 questions
1What does javac produce?
2Which part of the JVM reads .class files into memory?
3What does the JIT compiler do?
4A user types "abc" where your program expects a number. When is this problem found?

Key Takeaways

Was this tutorial helpful?

Lesson 13 of 482