Search⌘ K
AI Features

How Java Runs Code Behind the Scenes

Explore how Java executes programs behind the scenes by compiling source code into platform-independent bytecode and running it within the Java Virtual Machine. Understand the roles of the compiler, JVM, JIT optimization, and garbage collection to enhance debugging skills and write efficient, portable Java code.

Previously, we wrote a simple program, clicked “Run,” and saw output on the screen. It felt instantaneous, but behind that button click, a complex and elegant process was at work.

Java is distinct from many other languages because it doesn’t run directly on your computer’s hardware. Instead, it runs on a specialized software engine. Understanding this engine removes the magic from programming, helping us debug errors faster and write code that is efficient and portable.

The role of the JVM

The component that makes WORA possible is the Java Virtual Machine (JVM). The JVM is a program installed on a device. It serves as a consistent runtime environment for executing Java bytecode.

We can think about the process this way:

  1. We write Java source code (.java files).

  2. A compiler translates this code into bytecode (.class files).

  3. The JVM reads and executes that bytecode on any machine.

Because the JVM understands bytecode consistently everywhere, our program runs with the same semantics across operating systems.

Java execution pipeline: Write once, run anywhere
Java execution pipeline: Write once, run anywhere

Java’s long-term stability and performance come from this mature virtual machine rather than from the language syntax alone.

The two-step execution process

As we learned earlier, Java achieves its “Write once, run anywhere” capability through a unique two-step process involving compilation and execution. Let’s look at the mechanics of how this works.

  1. Compilation (source code to bytecode): We write human-readable source code in files ending with .java, which is the standard file extension for java. Our computer’s processor cannot understand this text directly. We must use the Java compiler (javac) to translate this source code into an intermediate format called bytecode. These files end with .class.

  2. Execution (bytecode to machine code): Bytecode is a universal language for the Java Virtual Machine (JVM). When we run the program, the JVM loads the .class files and translates the bytecode on-the-fly into native machine instructions that the specific operating system (Windows, Linux, or macOS) understands.

Because the JVM handles the specifics of the underlying operating system, we only need to compile our code once.

Executing the code

Knowing how to execute java program instructions manually is a foundational developer skill. Whether you are figuring out how to run a java file in terminal on a Mac/Linux or looking up how to run a java program from cmd on Windows, we use the command line to perform the two distinct steps we just learned: compiling and executing.

Step 1: Compiling with javac: To compile a java program, we first invoke the compiler command, javac, followed by the filename.

javac Hello.java
The command to compile source code

The compiler reads Hello.java. If there are no errors, it silently creates a new file named Hello.class in the same folder. This new file contains the bytecode.

Note: If you ever wonder how to read java class file contents, you cannot simply open it in a standard text editor. Because it contains compiled binary bytecode, it will look like gibberish. Instead, developers use a disassembler tool like the javap command in the terminal to view the human-readable instructions.

Step 2: Running with java: Next, to execute java program in command prompt or your system’s terminal, we start the JVM using the java command followed by the class name.

java Hello
The command to execute the compiled bytecode

The JVM looks for Hello.class, loads the bytecode, and executes it.

Important note: Notice we type java Hello, not java Hello.class. We tell the JVM the name of the class to load, not the specific filename.

Try it yourself

In the following playground, we have preloaded a simple file named Demo.java. We have provided a terminal so you can manually drive the compilation and execution process.

public class Demo {
    public static void main(String[] args) {
        System.out.println("Success! The JVM is executing your manually compiled bytecode.");    
    }
}
A minimal Java program to illustrate the execution pipeline

Instructions:

  1. Compile: Click the “Run” button to open the terminal window. Type javac Demo.java and press Enter. If successful, the terminal will simply show a new prompt.

  2. Verify: Type ls (list files) and press Enter. You should now see a new file named Demo.class listed next to the .java file. This is your bytecode!

  3. Execute: Now type java Demo and press Enter. You should see the success message printed to the console.

If you are performing these steps on your own local computer’s command prompt or terminal, remember that commands for listing files vary by operating system. On macOS and Linux, you use ls. On Windows, you use dir to confirm the .class file was created.

Compile time vs. runtime

Because Java has this two-step process (compilation then execution), errors can occur at two distinct times. Distinguishing between them is a critical skill for any developer.

  1. Compile time: This happens when the javac compiler reads our source code. The compiler checks for syntax errors, such as missing semicolons, misspelled keywords, or trying to store text in a number variable. If the compiler finds any error, it refuses to generate the .class file. We cannot run the program until these are fixed.

  2. Runtime: This happens after compilation, when the JVM is executing the bytecode. Run-time errors (exceptions) occur when the code is syntactically correct but tries to do something illegal during execution, such as dividing by zero or trying to open a file that doesn't exist.

Let’s look at a code example that illustrates the difference.

Java 25
public class ExecutionFlow {
public static void main(String[] args) {
System.out.println("Starting program...");
// Compile-time success: The syntax is perfect.
// Run-time Error: Division by zero is mathematically impossible.
System.out.println(5 / 0);
System.out.println("This line will never run.");
}
}
  • Line 3: The program starts successfully and prints the first message.

  • Line 7: The compiler sees valid Java syntax (numbers and a division symbol), so it creates the bytecode. However, when the JVM tries to execute this line, it encounters an impossible mathematical operation. The JVM stops the program immediately and reports an ArithmeticException.

  • Line 9: This line never executes because the program crashed on line 7.

The ecosystem: JDK vs. JRE vs. JVM

Before learning how to make an app using Java, we must set up our environment. During this process, we encounter three acronyms that represent a nested hierarchy of tools.

  1. Java development kit (JDK): The JDK is the toolkit we install as developers. It includes the Java compiler (javac), the JVM, core libraries and development tools (debuggers, documentation tools, etc.). Modern Java distributions focus on the JDK; older setups also provided the JRE separately.

  2. Java runtime environment (JRE): Historically, the JRE was a package containing the JVM and the standard libraries (code for Math, Strings, Networking, etc.) needed to run Java applications. Its purpose was to run Java programs, not compile them.

  3. Java virtual machine (JVM): The JVM is the engine that loads and executes bytecode. It sits at the heart of Java’s run-time system.

A simple way to visualize the relationship between JDK, JRE, and JVM
A simple way to visualize the relationship between JDK, JRE, and JVM

Smart execution: JIT and garbage collection

The JVM does more than just translate bytecode; it actively optimizes our program while it runs.

Just-In-Time (JIT) compilation

Originally, interpreters were slow because they translated one line of bytecode at a time. Modern JVMs use a JIT compiler. It monitors the code as it runs and identifies the sections of code that run frequently (like loops). The JIT compiler translates these hotspots directly into native machine code and stores them. The next time that code runs, the JVM uses the super-fast native version instead of interpreting it again. When evaluating java vs c++ performance, this dynamic optimization allows Java to remain highly competitive, as it adapts to the hardware in ways static compilers cannot.

Garbage collection (GC)

In older languages, programmers had to manually reserve memory for data and manually release it when finished. Forgetting to release memory caused memory leaks, eventually crashing the computer.

Java automates this with garbage collection. The JVM creates a background process that periodically checks the memory (the Heap). If it finds objects our program no longer uses or references, it automatically deletes them and reclaims the space. This memory management makes Java applications more stable and developers more productive.

Putting it all together

We have now traced the full journey of a Java program. It begins as human-readable source code, is compiled by javac into portable bytecode, and is finally executed by the JVM using the java command. Along the way, the JVM acts as our personal assistant, optimizing performance with JIT compilation and managing memory with the garbage collector. With this foundational understanding of how the engine works, we are ready to start feeding it data.