Free lesson · Java Basic To Advance
How Java Works
The Full Picture
Topics 1.1-1.7 established what Java is, what's installed when you install Java, and how to write and run a basic program. This final Topic in Module 1 puts it all together: what actually happens, end to end, when you run a Java program.
This is the Topic where everything we've covered connects. By the end, you should be able to trace a Java program from source code on disk to native CPU instructions executing — and explain it to an interviewer. Nikhil joins for the execution-engine sections since runtime model material is his domain.
The Lifecycle of a Java Program
A Java program goes through several distinct phases from source to execution:
- Write — you write
.javasource files - Compile —
javacproduces.classbytecode files - Package — optionally, bundle
.classfiles into JAR archives - Load — the JVM reads
.classfiles into memory - Verify — the JVM checks the bytecode for safety
- Link — resolve references between classes
- Initialize — run static initializers
- Execute — run bytecode through interpretation and JIT compilation
- Manage memory — garbage collection runs throughout
- Exit — when
main()returns orSystem.exit()is called
Most of these happen invisibly. Understanding them is essential for senior interviews.
graph TD
A[Source .java files] -->|javac compiler| B[Bytecode .class files]
B -->|optional: jar tool| C[JAR archive]
C -->|java command starts JVM| D[Class Loader]
B -->|java command starts JVM| D
D --> E[Bytecode Verifier]
E --> F[Linker]
F --> G[Class Initializer]
G --> H[Execution Engine]
H -->|interpretation| I[Bytecode interpreted]
H -->|JIT compilation| J[Native code execution]
K[Garbage Collector] -.->|background| H
style B fill:#7c3aed,color:#fff
style H fill:#10b981,color:#fff
style K fill:#f59e0b,color:#fff
This diagram shows the complete lifecycle of a Java program. You start with Java source files, which the javac compiler turns into bytecode .class files. These can optionally be packaged into a JAR archive. When you run the java command, the JVM starts and the class loader reads the bytecode into memory. The bytecode verifier checks it for safety. The linker resolves references between classes. The class initializer runs static initializers. Then the execution engine takes over, running bytecode through a combination of interpretation and JIT compilation. Throughout execution, the garbage collector runs in the background reclaiming memory.
Phase 1: Compilation
When you run javac HelloWorld.java, the compiler does several things:
Lexical analysis (tokenization). The source text is broken into tokens — public, class, HelloWorld, {, etc.
Parsing. Tokens are organized into an Abstract Syntax Tree (AST) representing the program's structure.
Semantic analysis. The compiler checks types, resolves names, verifies that everything the code refers to exists, and enforces language rules.
Code generation. The AST is translated into JVM bytecode.
Output. The bytecode is written to .class files — one per class defined in the source.
The output .class file is a structured binary format defined by the JVM specification. It contains:
- Magic number (
0xCAFEBABE— yes, really) - Version number (which Java version the class is compatible with)
- Constant pool (strings, numbers, type references)
- Access flags (public, final, etc.)
- Class hierarchy info (superclass, interfaces)
- Field and method definitions
- Bytecode for each method
- Attributes (line numbers, debug info, annotations)
graph TD
A[Source code text] --> B[Tokens]
B --> C[AST]
C --> D[Symbol table + type checks]
D --> E[Bytecode]
E --> F[.class file]
style A fill:#7c3aed,color:#fff
style C fill:#3b82f6,color:#fff
style E fill:#10b981,color:#fff
style F fill:#f59e0b,color:#fff
This diagram shows the compilation pipeline. Source code text is tokenized into atomic chunks. The tokens are parsed into an Abstract Syntax Tree representing the program's structure. The compiler then analyzes the AST for semantic correctness — building a symbol table and checking that all types are valid. Finally, bytecode is generated and written to .class files. Each step transforms the program into a representation closer to what the JVM can execute.
You can inspect the result of compilation with javap. Running javap -v HelloWorld shows the full class file structure including the constant pool and bytecode.
Phase 2: Loading
When you run java HelloWorld, the JVM starts and begins loading classes. Loading is more sophisticated than it sounds.
Class loaders form a hierarchy:
- Bootstrap class loader — loads core Java classes (
java.lang,java.util, etc.) from the JDK's internallib/modulesfile - Platform class loader — loads platform extensions
- Application class loader (also called System class loader) — loads your application's classes from the classpath
Loading is lazy. Classes aren't loaded all at once. They're loaded when first referenced. The first reference to String triggers loading of String. The first reference to your HelloWorld class triggers loading of HelloWorld.
Delegation. When a class loader is asked to load a class, it first asks its parent. Only if the parent can't find the class does the child loader try. This prevents user code from overriding core classes — you can't create your own java.lang.String and have it replace the real one.
This hierarchy matters for advanced use cases (custom class loaders for plugins, hot reloading, application servers). For most code, it's invisible.
Interactive animation: Class loading, parents first - bootstrap, platform and application loaders, which is why you cannot replace java.lang.String.
Phase 3: Verification
Before executing loaded bytecode, the JVM verifies it. The bytecode verifier checks:
- Operand stack manipulation is correct (no underflow, no overflow)
- Local variable types are consistent
- Branches target valid bytecode addresses
- Final classes aren't extended
- Private members aren't accessed from outside their class
- Method signatures match
- Types are consistent across operations
Verification protects the JVM from malformed or malicious bytecode. Even if someone hand-crafts a .class file to violate Java's rules, the verifier catches it before execution. This is part of Java's security guarantee.
The verifier rejects code that fails any check with a VerifyError. This is rare in practice because javac produces correct bytecode, but it can happen with manually-modified class files or class files from incompatible Java versions.
Phase 4: Linking and Initialization
Linking resolves symbolic references in the bytecode to actual memory addresses. When code references java.lang.String.length(), the linker finds the actual String.length method and replaces the symbolic reference with a direct one.
Initialization runs the class's static initializers — static blocks and static field initializers — exactly once per class loader.
public class Example {
private static int counter;
static {
// This block runs once when the class is first loaded
counter = 42;
System.out.println("Static initializer ran");
}
}
The order matters: static initializers run in source order, top to bottom. Cross-class initialization order is more complex (the JVM has specific rules to avoid circular dependencies).
Phase 5: Execution (Nikhil's Domain)
Nikhil here. Execution is where the JVM does its most sophisticated work. We covered the components in Topic 1.5 — here we look at how they interact during actual program execution.
Initial execution: interpretation.
When a method is first called, the JVM's interpreter executes its bytecode one instruction at a time. Interpretation is slow — there's overhead per instruction — but it works for any code, requires no compilation time, and gathers data about how the code behaves.
Tiered compilation: Sparkplug and TurboFan-equivalents.
(Java doesn't use those exact names — those are V8's components from the JavaScript world — but Java has the same concept.)
As code runs, the JVM counts how often each method is called. Methods called frequently are "hot" and benefit from compilation. The JVM has multiple JIT compilers at different levels:
- C1 (Client Compiler) — fast compilation, moderate optimization. Used for code that's hot but not super-hot.
- C2 (Server Compiler) — slow compilation, aggressive optimization. Used for the hottest code.
- Graal — alternative JIT in newer JVMs (since Java 9+), sometimes faster than C2.
The progression: a method starts interpreted. After being called enough times, C1 compiles it. After being called many more times (and observing what types it sees), C2 compiles it with full optimization.
graph LR
A[Method called once] -->|interpreted| B[Bytecode interpretation]
B -->|called many times| C[C1 compilation<br/>fast, basic opts]
C -->|called many many times<br/>+ stable types| D[C2 compilation<br/>aggressive opts]
D -->|type assumptions break| B
style B fill:#94a3b8,color:#fff
style C fill:#3b82f6,color:#fff
style D fill:#10b981,color:#fff
This diagram shows tiered JIT compilation in the JVM. A newly-called method starts in the interpreter — its bytecode is executed one instruction at a time. As the method is called repeatedly, the JVM identifies it as a hot method and compiles it with C1, the client compiler, producing moderately-optimized native code. If the method continues to be called frequently and the JVM observes stable type behavior, C2 the server compiler kicks in with aggressive optimizations. If C2's assumptions about types or behavior turn out to be wrong, the optimized code is invalidated and execution falls back to the interpreter — a process called deoptimization.
Deoptimization.
C2's optimizations are speculative. It compiles based on assumptions: "this method always receives integers," "this object is always a specific subclass," "this branch is never taken." If those assumptions break, the optimized code is invalidated and execution falls back to interpreted or C1-compiled code.
Deoptimization is expensive — work is wasted. Stable code that doesn't trigger deoptimization performs best. Code that mixes types, has surprising polymorphism, or has rare exceptional paths can defeat optimization.
You can observe JIT activity with JVM flags:
java -XX:+PrintCompilation HelloWorld
# Prints each method as it's compiled
java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation HelloWorld
# Detailed log of compilation decisions
For performance-sensitive code, watching JIT activity is the key diagnostic technique.
Phase 6: Memory Management
Throughout execution, the garbage collector runs in parallel reclaiming memory. We cover GC in detail in Module 19. The key concepts at this stage:
- Objects live on the heap
- The heap is divided into generations (young, old) based on object lifetime
- The GC periodically identifies unreachable objects and reclaims their memory
- Modern GCs (G1, ZGC, Shenandoah) try to minimize pause times
- The GC algorithm is configurable via JVM flags (
-XX:+UseG1GC,-XX:+UseZGC)
For a basic program like HelloWorld, the GC barely runs — the program ends before significant garbage accumulates. For long-running applications, GC behavior is a major performance factor.
Phase 7: Exit
When main() returns normally, or System.exit(code) is called, or all non-daemon threads finish, the JVM begins shutdown:
- Shutdown hooks run (registered with
Runtime.getRuntime().addShutdownHook()) - Finalizers run (legacy mechanism, deprecated)
- The JVM exits with an exit code
The exit code is 0 by default (success), or whatever was passed to System.exit(code). The shell sees this exit code in $? (Unix) or %ERRORLEVEL% (Windows).
Putting It All Together
A trace of what happens when you run java HelloWorld:
- Shell starts the
javaprocess - JVM initializes — sets up memory regions, starts threads, loads core libraries
- Application class loader looks for
HelloWorld.classin the classpath - JVM loads
HelloWorld.class— reads the file, parses the class structure - Bytecode verifier checks
HelloWorld's bytecode for safety - Linker resolves references in
HelloWorldto other classes (it referencesSystem,String,PrintStream) - Those referenced classes get loaded, verified, linked, initialized in turn (most are already loaded from earlier startup)
- JVM runs static initializers on
HelloWorld(none in this case) - JVM looks up the
mainmethod - JVM begins executing
main's bytecode in the interpreter maincallsSystem.out.println— interpreted bytecode invokes the methodprintlnwrites "Hello, world!" to standard outputmainreturns- JVM begins shutdown
- JVM exits with code 0
For HelloWorld, this all happens in tens of milliseconds. For a large enterprise application, the equivalent process can take seconds (JVM startup is part of why Java has a reputation for slow startup).
Interactive animation: java HelloWorld traced end to end - start the JVM, load and verify, initialize, run main, then exit with code 0.
Common Pitfalls
Warning: Confusing compile-time errors with runtime errors. Compile-time errors come from
javac. Runtime errors come from the JVM during execution. The two have different causes and fixes.Thinking JIT compilation is the same as
javaccompilation. They're different.javacproduces bytecode; JIT produces native code at runtime. Both are "compilation" but at different stages.Underestimating verification. It happens to every class, every time. It's part of why Java is safer to run untrusted code than C.
Forgetting static initialization order. Static initializers run when a class is first loaded, not when the program starts. Cross-class initialization can produce surprising ordering bugs.
Why This Matters in Interviews
Interview: "Walk me through what happens when you run a Java program" is a common senior-level interview question. The expected answer covers: compilation to bytecode, JVM startup, class loading (with the hierarchy), verification, linking and initialization, execution through interpretation and JIT, and finally exit. Candidates who can articulate this entire flow signal real depth.
Topic 26.12 has the full senior-candidate answer with sample phrasing.
Try It
Exercise: Trace the execution of HelloWorld in real time:
- Compile HelloWorld with
javac HelloWorld.java- Run with class loading traced:
java -verbose:class HelloWorld 2>&1 | head -40
- You'll see every class that gets loaded — note the order and the loader
- Run with JIT compilation traced:
java -XX:+PrintCompilation HelloWorld
- For a tiny program, you won't see much, but try running a longer-running program
- Run with GC tracing:
java -Xlog:gc HelloWorld
- You'll see GC events (probably none for HelloWorld since it ends too quickly)
Try the same with a longer program — write a loop that creates many objects, run it with these flags, and see what the JVM is doing internally. The flags expose the machinery that's normally hidden.
This is Module 1 complete. Module 2 covers Java's syntax fundamentals — variables, types, operators — which is where you start writing real code rather than just understanding the platform.