Java source code becomes runnable on the JVM by being compiled into class files, which hold bytecode and metadata rather than the original source text.

Learning Question

What does javac produce, and how is that different from running the program?

Compilation is easy to confuse with execution because both are part of “running Java.” They are different phases.

javac reads Java source files, checks whether the program is valid according to Java language rules, and emits .class files. The compiler does not start the application, allocate the application’s heap objects, perform garbage collection, or run main.

The first mental model is:

javac turns Java source into JVM-loadable class files. The JVM later turns those class files into runtime behavior.

What Compilation Does

Compilation from .java to .class includes several kinds of work:

  • parsing source text into a structured representation
  • checking names, types, access rules, and language constraints
  • resolving source-level references enough to validate the program
  • translating methods into bytecode
  • writing class metadata, method metadata, field metadata, constants, and attributes
  • optionally writing debug-related metadata such as line numbers and local variable tables

The output is not a native executable. It is a class file format understood by JVM implementations.

One source file may produce more than one class file. Nested classes, anonymous classes, local classes, records, enum support structures, and lambda-related artifacts can affect what appears in the compiled output. The exact details vary by language feature and compiler behavior, but the durable point is that the JVM consumes class files, not source files.

What Compilation Does Not Do

Compilation does not mean the program has run.

javac does not:

  • load classes into a running application JVM
  • execute static initializers as application behavior
  • choose the exact runtime classpath used by production
  • allocate normal application objects on the Java heap
  • decide which code will become hot
  • run garbage collection
  • produce final optimized machine code for every method

Some errors are caught at compile time because the compiler has enough information to reject the source. Other errors appear only when a JVM tries to load, link, initialize, or execute the program.

For example, a syntax error is a source-level compilation problem. A missing class at runtime is a class loading problem. A method that throws NullPointerException is an execution problem. A method that becomes faster after warmup is a runtime optimization phenomenon.

Source-Level Meaning vs. Bytecode-Level Form

Java source code is written in language constructs such as classes, methods, fields, expressions, loops, exceptions, generics, lambdas, and annotations.

Class files represent those constructs in a different form. Methods contain bytecode instructions. Type and member names appear through constant-pool entries and descriptors. Some source features are represented directly, while others are translated into lower-level class-file patterns.

For example:

  • a Java method becomes method metadata plus bytecode
  • a field becomes field metadata
  • an int parameter becomes a descriptor entry using JVM type notation
  • source line information may become optional debug metadata
  • generic type information is mostly enforced by the compiler and represented differently at runtime than ordinary object types

This is why reading bytecode is not the same as reading source code. Bytecode preserves executable structure for the JVM, not the source author’s exact text.

Build Tools Do Not Change the Boundary

In real projects, developers often use Maven, Gradle, an IDE, or a framework build plugin instead of directly calling javac.

Those tools add dependency resolution, annotation processing, test execution, packaging, resource copying, code generation, or container image creation. They may produce JAR files or other deployable artifacts.

The core boundary remains the same:

Java source and dependencies
-> compiler and build tooling
-> class files and packaged artifacts
-> JVM runtime loads and executes them

A JAR file is usually a packaging format that contains class files and resources. It is not the same thing as a running Java application.

Why This Matters for Developers

Separating compilation from execution prevents common mistakes.

If the code compiles but fails in production, the compiler has only proven that the source passed compile-time checks under the compile-time classpath. It has not proven that the production runtime classpath, configuration, class loader behavior, initialization path, memory usage, or thread behavior is correct.

If a library version changes and a program fails with NoSuchMethodError, the source may have compiled against one version while runtime loaded another. That is not a Java syntax problem. It is a mismatch between compiled references and runtime class availability.

If an annotation processor generates code, the generated class files are still part of the runtime artifact. The JVM does not care whether a class file came from handwritten source, generated source, Kotlin, Scala, or another JVM language.

Core Mental Model

Keep these boundaries separate:

  • Source code is checked and translated by the compiler.
  • Class files are the JVM-facing output of compilation.
  • Build tools package and arrange class files and resources.
  • Runtime behavior begins when a JVM process loads and executes those artifacts.
  • Passing compilation does not guarantee runtime class loading, initialization, memory behavior, or performance.

Final Summary

javac turns Java source into class files that the JVM can load.

Compilation creates the JVM-loadable representation of the program, but execution begins later inside a JVM process with its own classpath, class loaders, memory, threads, and runtime decisions.