skip to content
everything.start.compileAndRun

compile and run

javac → .class → java. what each step actually produces.

chapter
chapter 01.04
reading time
7 min read

javac Main.java checks your code and writes Main.class: bytecode, not machine code. java Main starts a JVM, loads that class and calls main. since java 11 you can skip the first step: java Main.java compiles in memory and runs it straight away.

the picture

two programs, two jobs. javac is a translator: it reads your source once and writes a file. java is a performer: it starts a JVM and plays that file, every time you run it. they don't even have to be on the same machine.

hover a stage.

hover a stage. the left half happens once, when you build. the right half happens every time the program runs.

step 1: javac

terminal
$ javac Main.java
$ ls
Main.class  Main.java

no output means it worked. javac is quiet on success and loud on failure. it reads the whole file, checks every type, and refuses to write anything if one thing is wrong:

terminal
$ javac Bad.java
Bad.java:3: error: incompatible types: String cannot be converted to int
        int x = "five";
                ^
1 error

file, line, what's wrong, and a caret under the exact spot. read compiler errors top to bottom and fix the first one first: later errors are often just the first one echoing.

one source file can hold several classes, and every class gets its own .class file, even the ones you didn't name:

terminal
$ javac Two.java     # Two, plus a record, an inner class, an anonymous class and a helper
$ ls *.class
Helper.class  Two$1.class  Two$Inner.class  Two$Point.class  Two.class

the $ names are nested classes, and Two$1 is the anonymous one (they get numbers). more in nested classes.

step 2: what's in a .class

a class file is a small binary format: a header, a constant pool (every class name, method name and string literal the code mentions), then the fields and methods, each method with its bytecode. you can open it with javap, the disassembler that comes with the JDK:

terminal
$ javap -c Main
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
 
  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String hello, world
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

notice the constructor you never wrote: if a class has none, javac adds one that just calls Object's. and notice main is four instructions. the JVM is a stack machine: instructions push values onto an operand stack and pop them off, there are no registers.

look at =
… 398 more bytes

major version: 0x45 = 69 = java 25. a java 21 JVM (max 65) refuses to load this file.

switch between the header and the code. in the bytecode view, step through main and watch the operand stack: two pushes, one call that pops both. the bytes are the real ones from this exact file.

the full format is its own chapter: the class file and bytecode.

step 3: java

terminal
$ java Main
hello, world

Main here is a class name, not a file name. the launcher starts a JVM, which searches the classpath (by default, the current directory) for Main.class, loads it, verifies it, and calls main. compiled somewhere else? point the classpath there:

terminal
$ javac -d out Main.java      # write .class files into out/
$ java -cp out Main
hello, world
predict the output
terminal
$ javac Main.java
$ java Main
hello, world
# now edit Main.java to print "hello, java", save, and:
$ java Main

skipping javac

for one-file programs, the two-step dance is overkill. since java 11 (JEP 330) the launcher can take a source file directly, and since java 22 (JEP 458) that program can use other source files sitting next to it.

run it =
$ javac Main.java # writes Main.class~224 ms
$ java Main # a class name, not a file~25 ms
hello, world

compile once, run as often as you like. this is what build tools (maven, gradle) do for you, for hundreds of files at a time.

the folder, afterwardsevery version
  • Main.javasource
  • Main.classnew, 414 bytes
switch modes and watch the folder. only the classic route writes a .class to disk. timings are medians of 15 runs on an arm mac: the in-memory compile costs about 175 ms every single run.

every runnable example on this site works with java Main.java. for anything bigger than a handful of files, use a build tool (maven or gradle): they run javac for you, only on what changed, and put the classes and dependencies on the classpath.

versions have to line up

the major version in the header (69 for java 25) is a promise: "this file needs at least this JVM". an older JVM won't even try:

terminal
$ java Main     # compiled with javac 25, run on a java 21 JVM
Error: LinkageError occurred while loading main class Main
	java.lang.UnsupportedClassVersionError: Main has been compiled by a more recent version of the Java Runtime (class file version 69.0), this version of the Java Runtime only recognizes class file versions up to 65.0

the fix is either a newer JVM, or telling javac which java you're targeting. --release checks your code against that version's API too, so you can't accidentally call a method that doesn't exist there yet:

terminal
$ javac --release 21 Main.java
$ xxd Main.class | head -1
00000000: cafe babe 0000 0041 001d 0a00 0200 0307  .......A........

0x41 is 65: java 21. the rule is java version + 44:

java81117212527
class file major version525561656971
keylines
  • javac turns .java into .class (bytecode). one .class per class, nested ones included.
  • java Main takes a class name and runs it on a JVM. java Main.java compiles in memory first.
  • the JVM is a stack machine. javap -c shows you exactly what javac produced.
  • a .class needs a JVM at least as new as its major version. --release targets older ones.
  • edited the source? recompile. the JVM never looks at your .java.