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.
step 1: javac
$ javac Main.java
$ ls
Main.class Main.javano 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:
$ javac Bad.java
Bad.java:3: error: incompatible types: String cannot be converted to int
int x = "five";
^
1 errorfile, 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:
$ 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.classthe $ 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:
$ 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.
major version: 0x45 = 69 = java 25. a java 21 JVM (max 65) refuses to load this file.
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
$ java Main
hello, worldMain 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:
$ javac -d out Main.java # write .class files into out/
$ java -cp out Main
hello, world$ javac Main.java
$ java Main
hello, world
# now edit Main.java to print "hello, java", save, and:
$ java Mainskipping 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.
$ javac Main.java # writes Main.class~224 ms$ java Main # a class name, not a file~25 mshello, 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.
- Main.javasource
- Main.classnew, 414 bytes
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:
$ 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.0the 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:
$ 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:
| java | 8 | 11 | 17 | 21 | 25 | 27 |
|---|---|---|---|---|---|---|
| class file major version | 52 | 55 | 61 | 65 | 69 | 71 |
javacturns.javainto.class(bytecode). one.classper class, nested ones included.java Maintakes a class name and runs it on a JVM.java Main.javacompiles in memory first.- the JVM is a stack machine.
javap -cshows you exactly what javac produced. - a
.classneeds a JVM at least as new as its major version.--releasetargets older ones. - edited the source? recompile. the JVM never looks at your
.java.