Stage 8: Concurrency and the JVM, lesson 7 of 7

Class loading and the JIT compiler

Advanced3 min readall versions
Explain it forThe essentials plus production detail and pitfalls.

Class loading happens lazily, the first time a class is used:

  1. Loading: a class loader finds the bytes, from a JAR or the module path.
  2. Linking: verification (the bytecode is safe), preparation (static fields get default values) and resolution.
  3. Initialisation: static initialisers and static field assignments run, once and thread-safely.

Loaders form a hierarchy: bootstrap (the core JDK), platform, and application (your classpath). Each asks its parent first, so your code can't replace java.lang.String.

Execution starts in the interpreter. The JVM counts how often methods run, and hot methods are compiled to native code by C1 (quick) and then C2 (heavily optimised). The JIT inlines small methods, removes dead code and uses runtime profiles, which is why Java gets faster after warming up.

Startup keeps improving: class-data sharing and Java 24 to 26's ahead-of-time caches (Project Leyden) reuse work from earlier runs.

Example

Java
public class Config {
    static {
        System.out.println("Config initialised");       // runs once, on first real use
    }
    static final String NAME = "JavaAtlas";              // compile-time constant: inlined, no init
    static String region = loadRegion();                 // reading this triggers initialisation

    static String loadRegion() { return System.getenv().getOrDefault("REGION", "ap-south-1"); }
}

System.out.println(Config.NAME);     // prints JavaAtlas, but doesn't initialise Config
System.out.println(Config.region);   // "Config initialised", then the region
See it happen
# Watch classes load and methods get compiled
java -Xlog:class+load:file=classes.txt -jar app.jar
java -XX:+PrintCompilation -jar app.jar | head

# Java 25+: record a training run, then start faster from the AOT cache
java -XX:AOTCacheOutput=app.aot -jar app.jar     # run a typical workload, then stop
java -XX:AOTCache=app.aot -jar app.jar           # later starts reuse the cache

Common mistake

Measuring performance right after startup. Hot code hasn't been compiled yet; use JMH for micro-benchmarks, with warm-up iterations.

Under the hood

The holder-class singleton works because class initialisation is lazy and thread-safe. ClassNotFoundException means code asked for a class by name at runtime and it wasn't found; NoClassDefFoundError means a class present at compile time is missing, or failed to initialise, at runtime, often because of a dependency conflict. Tools that reload code use separate class loaders, the source of confusing "same class, different loader" ClassCastExceptions.

Check yourself

Which JIT compiler produces the most optimised code?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Upgrade from Java 8 to Java 25.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.