Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Compiling, Running, and Inspecting Java Programs

Hello! Last lesson followed a Java class from source code to a class file and then into a running JVM. This time, you’ll observe that first boundary yourself: compile a program with javac, run its class file with java, and inspect its instructions with javap.

The commands are short, but the useful question is more substantial: what evidence does each tool give you, and what can’t it tell you? You can split the lesson into three sessions of roughly 15 minutes.


Compile and run the class

Work in a new, otherwise empty directory. Check that the commands come from a JDK you intend to use—Java 21 for this walkthrough:

java --version
javac --version
javap -version

The version strings need not be formatted identically, but if the tools report different JDK releases, resolve that mismatch before interpreting later results.

The Dev.java guide establishes the file-naming and output-directory conventions behind the commands. Read only the indicated portions; its broader option reference is not needed yet.

Javac - the Compiler - Dev.java

Read Dev.java’s “Javac - the Compiler” to establish where the source file goes, what javac produces, and how a separate output directory can be chosen.

Under “Introducing javac” → “Description,” read file naming, from the sentence about .java and .class suffixes through the sentence about both files in a directory. Under “Standard Options,” find --class-path and read the default, stopping at the bullet about the current directory. Then find -d directory and read its destination rule, stopping after the explanation of package subdirectories.

Use the Journey class from the previous lesson. If you did not save it, create Journey.java in your working directory with this content:

public class Journey {
    static int score(int limit) {
        int total = 0;
        for (int i = 0; i < limit; i++) {
            total += i;
        }
        return total;
    }

    public static void main(String[] args) {
        int result = 0;
        for (int i = 0; i < 100_000; i++) {
            result += score(i % 100);
        }
        System.out.println(result);
    }
}

From that directory, run:

javac Journey.java
java -cp . Journey

You should see:

161700000

A successful javac normally says nothing; it creates Journey.class alongside the source file. You can check the directory with ls on Unix-like shells or PowerShell, or dir in Windows Command Prompt. The java command takes the class name, not Journey.class: the launcher finds that class on the class path and calls its main method. Here -cp . makes the working directory explicit, so the command does not depend on a pre-existing CLASSPATH setting.

The result is predictable without executing the program. Each set of 100 calls uses limits from 0 through 99; that set contributes 161,700. The outer loop repeats it 1,000 times. This check matters: a successful launch proves the class ran, while the expected value helps establish that you ran the intended program.


Inspect what javac emitted

Running the class tells you its behavior, not its stored representation. Oracle’s Java 21 reference describes javap as a class-file disassembler: it reads a compiled class and presents information about its members and, when requested, its bytecode.

The javap Command

Read the Java 21 javap reference for the distinction between its default member listing and instruction disassembly.

In “Description,” read basic behavior, from the opening explanation through the sentence describing default output. In “Options for javap,” read the relevant options, starting at -verbose and stopping after -c. Focus on -private (-p) for member visibility and -c for instructions; you do not need the full verbose listing today.

First ask for the member-level view, then for disassembly:

javap -cp . Journey
javap -cp . -c -p Journey

The first command should show a constructor, score(int), and main(String[]). You did not write the constructor: javac supplied a default one. The second command adds each method’s Code listing. -p requests all members, including private ones; this class does not require it to reveal score, but it is useful when inspecting unfamiliar production classes.

For a guided view of similar output, watch these portions of Oracle Developers’ Java Bytecode Crash Course:

Java Bytecode Crash Course

Oracle Developers’ “Java Bytecode Crash Course” shows why javap is useful and walks through an integer loop. Its example is different from Journey, so compare the instruction patterns rather than expecting identical listings.

Watch using javap for the tool’s role, then the integer loop for local-variable slots, stack operations, and branches. The video’s loop is in an instance method, where slot 0 holds this; Journey.score is static, so its limit parameter occupies slot 0 instead.

Find score in your own javap -c -p output. A typical Java 21 javac listing begins by putting zero into local slots for total and i. It then loads i and limit for a comparison. In the loop body, iload instructions put the two integers needed for addition onto the operand stack; iadd consumes them, and istore saves the sum. An iinc updates the counter, while a backward goto repeats the loop. Finally, ireturn returns total.

Read the numbers at the left of the listing as bytecode offsets, not source-code line numbers or CPU addresses. The distinction is particularly useful when inspecting branches: a conditional instruction names the bytecode location where execution continues if its condition holds. The exact offsets are less important today than being able to identify the loop’s comparison, body, and exit.

Now find main. Its invokestatic call refers to Journey.score; later instructions obtain System.out and invoke println. These are instructions and references in a class file. javap is not displaying the native instructions that HotSpot might generate after profiling this program, and it is not reconstructing the original Java source.


Use the tools to distinguish source from artifact

One small change makes the boundary visible. After completing your first inspection, edit only the final print statement in Journey.java:

System.out.println(result + 1);

Run java -cp . Journey without compiling again. It should still print 161700000: the launcher is executing the existing Journey.class, not rereading your edited source. Then run:

javac Journey.java
java -cp . Journey
javap -cp . -c -p Journey

The output should now be 161700001. In main’s disassembly, look near the call to println for instructions that add one before printing. You have correlated a source edit, a newly generated class file, and an observable behavior change. That is a more reliable habit than assuming the source currently open in an editor matches the class being run.

If a command fails, diagnose the boundary it was meant to cross:

  • javac is unavailable: you may have a runtime without JDK tools, or your command path may point somewhere unexpected. Recheck the tool versions.
  • javac reports a public-class/file-name mismatch: save the source as Journey.java, preserving the class name’s capitalization.
  • java cannot find or load Journey: confirm Journey.class exists in the working directory and use java -cp . Journey, not java Journey.class.
  • Compilation succeeds but you still see old behavior: check which directory contains the class you are running, and recompile after the edit.

Java 21 can also launch a source file directly with java Journey.java. That is convenient for some tasks, but do not use it for this walkthrough: it obscures the separate class-file artifact you are trying to inspect.


Closing perspective

You have now used each tool for a distinct claim. javac produced a class file from source; java launched the compiled class and exposed its behavior; javap showed the class’s members and JVM instructions. None of those commands alone proves what optimized native code HotSpot eventually executed.

Keep the working Journey.class. The next lesson uses class files to examine Java’s portability promise—and where compatibility between compilers, class-file versions, runtimes, and environments places limits on “write once, run anywhere.”

Can't find a good explanation? Sign up and we'll make it for you

Sign up