Inside Java Compilation: From Source Code to Optimized Bytecode
This article explains Java's compilation pipeline—from source parsing, symbol table construction, and annotation processing to semantic analysis and bytecode generation—and then details HotSpot's runtime JIT compilation, covering client vs server compilers, inlining, devirtualization, escape analysis, and on‑stack replacement.
1. Compilation Process
Compilation turns Java source files into .class files and consists of four steps.
1. Preparation
Initialize the annotation processing tool (APT).
2. Parsing and Symbol Table Construction
Source code is tokenized, an abstract syntax tree (AST) is built, and a symbol table (a key‑value map) is populated with variable names, constants, method signatures, literals, generated temporary files, and language tags. The compiler’s later phases operate on the AST.
3. Annotation Processors
Annotation processors are compiler plugins that read or modify any AST element, often used for code generation (e.g., Lombok, MapStruct). If the AST is changed, the compiler repeats the parsing step, forming a “round”. This is the only way developers can influence compilation.
4. Analysis and Bytecode Generation
After a correct AST is produced, semantic analysis validates the tree, followed by bytecode generation.
4.1 Annotation Checks
Checks declarations, variable‑assignment compatibility, and performs constant folding (e.g., int a = 1 + 2; becomes int a = 3;).
4.2 Data and Control‑Flow Analysis
Verifies that local variables are initialized before use, that every method path returns a value, and that checked exceptions are properly handled.
4.3 Desugaring
Removes Java syntactic sugar such as autoboxing, generics, and var‑args, converting them to fundamental language constructs.
4.4 Bytecode Generation
The final phase emits .class files from the AST and symbol table, adding a small amount of extra code.
2. Runtime (JIT) Compilation
Runtime compilation compiles hot code to native machine code to avoid interpretation overhead. The HotSpot JVM combines an interpreter with a Just‑In‑Time compiler.
2.1 When Compilation Happens
The Sun JDK uses a method invocation counter; once the counter exceeds a threshold (default 1500 in client mode, 10000 in server mode) the method is compiled. The thresholds can be tuned with -XX:CompileThreshold.
2.2 Compilation Modes
Two JIT compilers are available: the lightweight client compiler (C1) and the heavyweight server compiler (C2).
2.2.1 Client Compiler (C1)
Key optimizations include:
Method inlining – replaces getter/setter calls with direct field access.
Devirtualization – inlines calls when the concrete type is known.
Redundant code elimination – removes dead code such as unreachable log statements.
Example of inlining:
Order o = new Order();</code>
<code>o.setTotalAmount(o.getOrderAmount() * o.getCount());After compilation:
Order o = new Order();</code>
<code>o.orderAmount = o.orderAmount * o.count;Inlining can be disabled with -XX:-Inline, but it is strongly recommended to keep it enabled.
Devirtualization example:
public interface Animal { void eat(); }</code>
<code>public class Cat implements Animal { public void eat() { System.out.println("Cat eat !"); } }</code>
<code>public class Demo { public void execute(Animal animal) { animal.eat(); } }If only Cat implements Animal, the compiled execute method becomes:
public void execute() { System.out.println("Cat eat !"); }Redundant elimination example:
private static final boolean isDebug = false;</code>
<code>public void execute() { if (isDebug) { log.debug("do execute."); } System.out.println("done"); }After C1 compilation the dead branch is removed:
public void execute() { System.out.println("done"); }2.2.2 Server Compiler (C2)
C2 performs global optimizations such as escape analysis, scalar replacement, stack allocation, and lock elimination.
Escape Analysis & Scalar Replacement
If an object does not escape the method, its fields can be replaced by scalar variables.
Point point = new Point(1, 2);</code>
<code>System.out.println("point.x = " + point.x + "; point.y" + point.y);After compilation:
int x = 1, y = 2;</code>
<code>System.out.println("point.x = " + x + "; point.y" + y);Stack Allocation
When escape analysis shows an object does not escape, C2 allocates it on the stack instead of the heap, making allocation faster and automatically reclaimed at method exit.
Lock Elimination
Synchronized blocks on non‑escaping objects are removed.
Point point = new Point(1, 2);</code>
<code>synchronized(point) { System.out.println("point.x = " + point.x); }After optimization:
Point point = new Point(1, 2);</code>
<code>System.out.println("point.x = " + point.x);2.3 On‑Stack Replacement (OSR)
OSR recompiles only the hot loop body of a method, allowing the compiled code to be used for subsequent iterations while the rest of the method continues to be interpreted.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JD Cloud Developers
JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
