How Java Works: Follow One File from javac to the JVM

Diagram of Main.java compiled by javac into Main.class bytecode, then loaded, verified and JIT-compiled by the JVM

How Java works comes down to two programs. javac compiles your source code into bytecode, and the Java Virtual Machine (JVM) loads that bytecode, checks it and runs it. The JVM interprets the bytecode at first and later compiles busy methods to native machine code. When you type java Main and press Enter, all of that happens before your program prints a single line.

This guide follows one small file, Main.java, through every stage. You run each step yourself in a terminal: write the source, compile it, run it, watch how the JVM speeds it up, and confirm the result. Along the way you'll see why most first-day Java errors happen and how to fix them.

In short
  • javac turns Main.java into platform-independent bytecode stored in Main.class.
  • The java command takes a class name (Main), not a file name (Main.class).
  • Before main runs, the class loader finds the class, verifies the bytecode and links it.
  • The JVM interprets bytecode first, then JIT-compiles hot methods to native code.
  • Most errors come from names, paths or mismatched versions, rarely from the JVM itself.
  1. 0:00Intro
  2. 0:24Get your tools ready
  3. 1:11Write Main.java with a main method
  4. 2:08Compile it with javac
  5. 3:04Run java Main and load the class
  6. 4:00Watch the JVM interpret, then JIT
  7. 4:55Confirm the output and versions
  8. 5:45Fix the most common errors
  9. 6:43From source to native code
  10. 7:16javac builds it, the JVM runs it

Get Your Java Tools Ready

The goal is to follow one Java file from source code to running bytecode on the JVM. To keep each stage easy to see, you'll work with a single small file and four steps: write, compile, run, and watch the JVM optimize. When you finish, you'll have a program that prints one line, and you'll know what happens at each stage behind it.

You need a JDK, the Java Development Kit. A runtime alone isn't enough, because the runtime doesn't include the javac compiler. You also need a terminal, and any shell will do. Basic Java syntax helps too, such as knowing what a class and a method are.

Check your setup before writing any code. Run javac -version in your terminal. If it prints a version number, you're ready. If the shell says the command isn't found, either the JDK isn't installed or its folder isn't on your PATH yet.

  • A JDK is installed, not only a Java runtime
  • A terminal is open in an empty working folder
  • javac -version prints a version number
  • You can recognize a class and a method in Java code

Write Main.java with a main Method

Open your editor, create a file called Main.java, and type a public class with a main method. This is the smallest complete program you can give the JVM. Every larger Java application starts from the same basic shape.

The first line declares public class Main. Inside it, public static void main takes a String array of arguments. The body calls System.out.println to print "Hello, JVM!", and then two closing braces end the method and the class.

Two details matter here. First, a public class must live in a file with the same name, including the capital M. If the names don't match, javac reports a compile error, not a runtime error. Second, the JVM looks for the exact signature public static void main(String args). That method is the entry point, where your program starts.

  • The file is named Main.java, with a capital M
  • The class is declared as public class Main
  • main is public, static and void, and takes a String array
  • Each opening brace has a matching closing brace
public class Main {
  public static void main(String[] args) {
    System.out.println("Hello, JVM!");
  }
}

Compile Main.java into Bytecode with javac

In the same folder, run javac Main.java. javac doesn't produce a Windows or Linux program. It produces bytecode, an instruction set designed for the JVM rather than for any particular operating system. The bytecode is written to a new file, Main.class, next to your source file.

If the compile succeeds, javac prints nothing. A silent javac is good news. When something is wrong, javac reports errors and warnings with a file name and line number, so you know where to look. If it prints nothing, the bytecode is on disk. Run ls to see Main.class beside Main.java.

To look inside, run javap -c Main. This disassembles the class into readable instructions. getstatic fetches System.out, ldc loads the string "Hello, JVM!", and invokevirtual calls println. Because these instructions target the JVM and not your hardware, you can copy the same Main.class to another operating system and it runs unchanged, provided a matching Java runtime is installed there.

  • javac Main.java finishes without printing anything
  • Main.class appears next to Main.java
  • javap -c Main shows getstatic, ldc and invokevirtual
javac Main.java
ls
javap -c Main

Run java Main and Load the Class

Type java Main to hand the bytecode to the JVM. Notice that you give the class name, Main, and not the file name. Before any line of your code runs, the JVM prepares the class in several stages. You don't see these stages when things go right, but knowing them explains most startup errors.

First, the class loader finds Main.class on the classpath and reads its bytes. Next, the verifier checks that the bytecode is well-formed and safe to run. Then the linker prepares memory and resolves references to other classes and members. Finally, the JVM initializes the class and calls main, and you see "Hello, JVM!" printed.

Now try java Main.class. The launcher treats Main.class as a class name, can't find a class with that name, and reports a ClassNotFoundException. The rule to remember is that javac takes a file path, while java takes a class name. This one difference causes a lot of first-day errors.

  • java Main prints Hello, JVM!
  • You don't add .java or .class after the class name
  • You can name the stages in order: load, verify, link, initialize, run main
java Main
java Main.class

Watch the JVM Interpret, Then JIT-Compile

Once main starts, the next question is speed. The JVM doesn't compile your whole program to machine code before running it. It starts by interpreting bytecode and watches which methods run often. This gives Java a quick start and fast code in long-running programs, and you don't have to change any of your code to get it.

Take a method that's called inside a loop. At first, the JVM interprets it one operation at a time. Each call raises a counter for that method. When the counter passes a threshold, the JVM marks the method as hot. HotSpot, the JVM used in common JDKs, then compiles it with its C1 compiler, and later with C2. Calls after that run as native machine code.

You can notice this as warm-up time. A long-running server often gets faster after it has handled some traffic, because its busiest methods have been JIT-compiled by then. And because compilation happens at runtime, the JIT can generate code for the exact processor it runs on. Portable bytecode doesn't mean slow code.

  • Interpret: the JVM runs bytecode one operation at a time
  • Count calls: each call raises the method's counter
  • Mark hot: past a threshold, the method becomes a JIT candidate
  • JIT compile: HotSpot uses C1, then C2
  • Run native: later calls execute machine code

Confirm the Output and Your Java Versions

With the four steps done, check two things: that the program prints what you expect, and that your compiler and runtime match. These checks take seconds, and they catch most setup problems before they turn into confusing bugs.

Run java Main and confirm you see "Hello, JVM!". Then run echo $? to see the exit code. A 0 means the program exited cleanly. Next, run javac -version and java -version. They should report the same major version. In this example, both report 21.

To test the full loop, change the message in Main.java, run javac again, and then run java Main. If you forget to recompile, you'll still see the old text, because the JVM only runs Main.class and never reads your source file. Versions matter too. Bytecode built by a newer compiler won't run on an older runtime, so use the same JDK for both steps.

  • java Main prints the expected message
  • echo $? prints 0
  • javac -version and java -version report the same major version
  • After you edit the message and recompile, the new text appears
java Main
echo $?
javac -version
java -version

Fix the Most Common Java Startup Errors

Even a tiny program runs into a few errors again and again. Most come from names, paths or versions. The JVM is almost always doing exactly what it was told, so check your setup first when something breaks.

ClassNotFoundException usually means the classpath or the class name is wrong. Run java from the folder that holds Main.class, or pass the right folder with -cp. A public class Main saved in Hello.java won't compile, so rename the file to Main.java. "javac: command not found" means no JDK is on your PATH. UnsupportedClassVersionError means the runtime is older than the compiler that built the class.

Java error output can be long, so read the Caused by line first. It usually names the exact class the JVM couldn't find. Packages also change names. If Main.java declares package app, the class's full name is app.Main, and you run it as java app.Main from the folder above app. A class name always includes its package.

  • ClassNotFoundException: check the working folder, -cp and the class name
  • Public class name doesn't match the file name: rename the file
  • javac not found: install a JDK and add it to your PATH
  • UnsupportedClassVersionError: use a runtime at least as new as javac
  • Class in a package: run it by its full name, such as app.Main

Trace the Full Path from Source to Native Code

Every Java program you run follows the same path, from a text file to code running on your processor. You write Main.java with a main method. javac compiles it to bytecode, and that bytecode lives in Main.class. When you run java Main, the class loader loads the class, the verifier checks it, and the linker prepares it.

The JVM then interprets the bytecode and JIT-compiles hot methods to native machine code. If the classpath or class name is wrong, you get a ClassNotFoundException before any of your code runs. In short, javac builds portable bytecode once, and the JVM runs it fast on any machine with a matching runtime.

  • Write Main.java with a main method
  • javac compiles it to bytecode in Main.class
  • The class loader loads it, the verifier checks it and the linker prepares it
  • The JVM interprets it, then JIT-compiles hot code

Key takeaways

  • javac compiles source into bytecode, and the JVM runs that bytecode.
  • Use a file path with javac (Main.java) and a class name with java (Main).
  • The class is loaded, verified and linked before main ever runs.
  • JIT compilation explains why Java programs often speed up after warming up.
  • Recompile after every edit, because the JVM only reads Main.class.
  • Keep javac and java on the same JDK version to avoid version errors.

Frequently asked questions

What is the difference between javac and java?

javac is the compiler. It reads a .java source file and writes bytecode to a .class file. java is the launcher. It starts the JVM, loads a class by name and runs its main method.

Why does java Main.class give ClassNotFoundException?

The java command expects a class name, not a file name. It treats Main.class as the name of a class, can't find one, and fails. Run java Main instead.

Is Java compiled or interpreted?

Both. javac compiles source code to bytecode ahead of time. The JVM then interprets that bytecode and JIT-compiles frequently used methods to native machine code while the program runs.

Can I run the same .class file on Windows, macOS and Linux?

Yes. Bytecode targets the JVM, not a specific operating system, so the same Main.class runs unchanged on any system with a matching Java runtime installed.

What does UnsupportedClassVersionError mean?

It means the class was compiled by a newer javac than the Java runtime you're using. Use the same JDK for compiling and running, or install a runtime that is the same version or newer.

Why does my program still print the old message after I edited it?

The JVM runs Main.class, not Main.java. If you edit the source without running javac again, the old bytecode still runs. Recompile, then run java Main.

Watch the full video on YouTube →

Souy Soeng

Souy Soeng

Hi there 👋, I’m Soeng Souy (StarCode Kh)
-------------------------------------------
🌱 I’m currently creating a sample Laravel and React Vue Livewire
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL, or Flutter
⚡ Fun fact: I love turning ☕️ into code!

Post a Comment

CAN FEEDBACK
Ad