You add a library, start the application, and eventually call one of its methods. Before that method can do useful work, the JVM needs a runtime type behind the class name. Having a .class file on disk does not mean its static initializer has already run.
Lesson 9 followed objects after their classes existed, including objects the JIT could eliminate. Now we move one step earlier: how do class-file bytes become a class the running JVM can use?
Loaded, linked, and initialized are different states. Linking comprises verification, preparation, and resolution, but these are not three boxes that must all finish eagerly before any execution. Keep that qualification in mind as we follow the lifecycle defined by the JVM specification.
Loading: bring class bytes into the runtime
Suppose the application needs com.example.Customer. A class loader locates its binary representation, perhaps in a JAR or in bytes generated by a framework. A custom loader can pass those bytes to ClassLoader.defineClass(...).
Loading creates two connected runtime pieces. HotSpot builds internal metadata that records the class’s fields, methods, parent types, and other details the VM needs. It also creates a java.lang.Class object on the Java heap. That is the object returned by expressions such as:
Customer.class
customer.getClass()
Java code uses this Class<Customer> object to inspect and refer to the type. HotSpot documentation and source code often call it the class’s mirror because it is the Java-facing representative of the VM’s internal class definition.
The word mirror does not introduce another object. It is just a HotSpot name for this familiar java.lang.Class object. Reflection starts from the mirror, while the VM keeps the fuller class representation in its own metadata. We will place both pieces in memory later in the lesson.
Class identity includes the defining loader
Imagine two plugins each bring a class called com.example.Customer. If separate loaders define those classes, the JVM has two distinct runtime types even if the bytes are identical:
class identity = binary name
+ defining class loader
An object of one Customer type cannot be cast to the other. That explains errors whose abbreviated message seems to say “Customer cannot be cast to Customer”: the displayed name hides the loader part of identity.
The defining loader is the loader that actually turns the bytes into the runtime type. A plugin loader may instead ask its parent to load Customer. If the parent defines Customer once, both plugins use that same type.
So two loader objects do not automatically mean two types. Two separate definitions of the same name do. When a cast fails across a plugin boundary, inspect both the class name and getClassLoader(); equal names with different defining loaders identify different runtime types.
Linking connects the loaded type to runtime state
Finding bytes is only the beginning. The JVM needs to establish that their instructions obey its rules, create the class state those instructions rely on, and connect symbolic references to runtime entities.
These stages answer different questions. Verification concerns valid execution; preparation concerns initial state; resolution concerns what a reference means. A class may be verified and prepared while some of its symbolic references are still unresolved.
Verification: can these instructions execute legally?
Bytecode can come from javac, another JVM language, an agent, or a bytecode generator. The JVM cannot assume every producer generates valid instructions. The verifier checks constraints such as operand-stack types, local-variable use, branch targets, and consistency where control-flow paths meet.
For example, iadd expects integer operands. Bytecode that supplies an object reference in place of an integer cannot be executed as a valid integer addition. Verification failure can produce VerifyError. Malformed class-file structure can fail earlier with ClassFormatError; not every bad file is a verifier failure. Access and member lookup also have resolution-time checks.
Preparation creates state before Java initialization logic
Use one small class to separate the stages:
class Config {
static int count = 42;
static Object owner = new Object();
}
Preparation establishes static fields and their JVM defaults: conceptually count is 0 and owner is null. It can also establish implementation data structures such as method tables. It does not execute the Java expressions 42 and new Object() as ordinary static initialization code.
Later, initialization performs the assignments described in the source. This separation is useful when reasoning about recursive initialization: partially initialized class state exists, even though straightforward Java access usually triggers initialization before letting you read a nonconstant static field.
Why a compile-time constant behaves differently
We just saw that an ordinary static field first receives its default value, then its Java initializer runs during class initialization. One common case follows a special path:
static final int LIMIT = 42;
static final Integer BOXED = 42;
LIMIT is a compile-time constant. The class file records 42 in a ConstantValue attribute, and code that reads Config.LIMIT may contain the value 42 directly. That read therefore does not need to initialize Config.
BOXED looks similar, but it is not a compile-time constant because Integer is a reference type. Assigning it requires runtime initialization work.
The point is narrow but important: static final alone does not make a field a compile-time constant. Primitive and String fields with constant-expression initializers can receive this treatment; other static fields still depend on class initialization. In the JVM’s specified initialization procedure, a ConstantValue is assigned before <clinit> runs.
Resolution gives symbolic references runtime meaning
A concrete call makes resolution easier to see:
class Customer {
static int fee() {
return 42;
}
}
class Checkout {
int total() {
return Customer.fee();
}
}
Checkout.class does not store a final memory address for Customer.fee(). The call instruction points to entry #7 in Checkout’s constant pool:
0: invokestatic #7
// Method Customer.fee:()I
Constant pool:
#7 Methodref #8.#9
// Customer.fee:()I
#8 Class #10
// Customer
#9 NameAndType #11:#12
// fee:()I
#10 Utf8 Customer
#11 Utf8 fee
#12 Utf8 ()I
The symbol says: find the class named Customer, then find its static method named fee with descriptor ()I—no parameters, returning an int.
When the reference is resolved, the JVM uses Checkout’s loading context to identify the correct runtime Customer class, looks up fee()I, and checks that the reference is valid and accessible. The runtime constant-pool entry can then represent the established runtime relationship.
If the loaded Customer class does not have that method, resolution can produce NoSuchMethodError. The source compiled against one API, but the symbolic description cannot be connected to the classes present at runtime. The next lesson opens the constant pool in more detail; here the key idea is that resolution turns a description into a checked runtime relationship.
Resolution may continue after initialization
A class can contain a reference used only on an optional code path. The JVM need not resolve that reference just because an unrelated method is executing.
Implementations may resolve eagerly or lazily within the specification’s rules for when errors become observable. Therefore, an initialized class can still encounter a resolution failure later. Initialization readiness does not prove that every optional dependency works. This is one reason a rarely used feature can expose a linkage error long after startup.
Initialization executes the class’s static logic
Extend Config with a static block:
class Config {
static int count = 42;
static {
System.out.println("initializing");
}
}
The compiler collects the required static field assignments and static blocks into the special class or interface initialization method, <clinit>. Here it assigns count, then prints the message, preserving source order. A class with no such work need not have a <clinit> method.
Before initializing a class, the JVM ensures it is verified and prepared. It initializes its superclass and the relevant superinterfaces that declare default methods before running the class’s own initializer. The procedure coordinates competing threads; successful initialization is not repeated for each access. Recursive initialization and failure have additional rules. If initialization fails, the type can enter an erroneous state, and later active uses can fail rather than retry the initializer normally.
<clinit> initializes the class. Constructors, represented by <init>, initialize individual instances. Creating a second Config instance does not rerun the static block.
First relevant use can trigger initialization
Creating an instance, invoking a declared static method, or accessing a declared nonconstant static field can require initialization. Certain reflective operations do too. Inherited field access initializes the class that actually declares the field, rather than necessarily the subclass name written in the expression.
A class literal such as Config.class does not itself trigger initialization. Reading a compile-time constant such as Config.LIMIT also does not require initialization of Config. This makes “first relevant use” more accurate than “the first mention of its name.”
Save this bounded experiment as LifecycleDemo.java. Predict when the message will appear:
class Config {
static final int LIMIT = 42;
static int count = 42;
static {
System.out.println("initializing");
}
}
public class LifecycleDemo {
public static void main(
String[] args)
throws Exception {
var type = LifecycleDemo.class;
var loader = type.getClassLoader();
Class.forName(
"Config", false, loader);
System.out.println("loaded");
System.out.println(Config.LIMIT);
System.out.println(Config.count);
System.out.println(Config.count);
}
}
Run javac LifecycleDemo.java, followed by java LifecycleDemo. The false argument requests loading without initialization. The first read of count then requires initialization before the read proceeds.
loaded
42
initializing
42
42
This output was verified with Homebrew OpenJDK HotSpot 25.0.2. The example demonstrates initialization timing; it does not expose the exact instant every linking operation occurs. javap -c -verbose Config also shows ConstantValue: int 42 for LIMIT, while the static initializer contains the assignment to count.
Deferring work avoids executing every library’s initialization code at startup. The trade-off is that a first request may pay for loading, linking, or initialization as well as JIT warmup. Those are separate causes of first-use latency.
Follow Customer across Metaspace and the heap
Consider this class after loading and initialization:
class Customer {
static int nextId = 1;
final int id = nextId++;
}
Loading created HotSpot’s Customer metadata and the heap object returned by Customer.class. Preparation established the ordinary static field with its default 0; initialization changed nextId to 1. A later new Customer() gives the new object id = 1 and advances the class-wide nextId to 2.
Customer allocation: the Class object gives Java code access to the type, while Metaspace holds much of the VM's detailed class representation. The arrows show VM associations, not ordinary Java reference fields. Open full-size diagram.In HotSpot, much of the class, method, field, type-relationship, and runtime-constant-pool metadata is allocated through Metaspace in native memory. The Class<Customer> object is a Java heap object associated with that metadata. HotSpot also stores static fields for an ordinary instance class with its mirror, while each Customer object carries only its own instance state.
Both the Class<Customer> object and each Customer instance are connected to the same runtime class definition. They serve different roles: one represents the type to Java code; the other is an instance of that type. Loading creates the type representation before the class’s initialization logic runs. JIT-compiled machine code, if produced later, belongs to the separate Code Cache.
Loader reachability can keep Metaspace occupied
Suppose each plugin reload creates a loader and defines another generation of plugin classes. Removing the plugin from a menu does not prove that its loader has become unreachable. A long-lived cache, thread context class loader, or retained plugin instance can keep the old loader reachable indirectly.
For ordinary named classes, unloading depends on the defining loader becoming reclaimable. A reachable loader prevents unloading its ordinary classes. Once the retention path is removed, unloading becomes possible, not immediate or guaranteed at the next collection; collector configuration and JVM policy matter. Bootstrap-defined classes are not unloaded, and hidden classes have special lifetime rules outside this lesson’s ordinary loader model.
This explains why a Metaspace problem can begin with a Java heap reference. The collector preserves what is still reachable. It cannot reclaim an old plugin generation merely because the application intended to retire it. Generating ever more classes in one long-lived loader can also grow metadata without repeated loader creation.
When investigating, correlate loaded and unloaded class counts with Metaspace used memory and inspect retention paths for retired loaders. Reserved address space, committed memory, and used metadata are different measurements; a large reservation alone is not evidence of a leak.
Check your reasoning
Config has been loaded and prepared. Its ordinary field is declared static int count = 42, but initialization has not executed. What belongs in the conceptual preparation snapshot?
Pause here, then check your reasoning
count = 0. The assignment to 42 belongs to the Java initialization logic. An ordinary Java read of the nonconstant static field normally triggers initialization first, so that conceptual snapshot is not a promise that the first normal read prints zero.
Now suppose two loaders delegate Customer to the same parent. Have they necessarily created distinct Customer types?
Check the identity boundary
No. Identity uses the defining loader. If the parent defines the class once and both children use that definition, they share the same runtime type. Separate definitions of the same binary name would create distinct types.
Connect the source file to execution
We can now connect the earlier object and memory lessons to the type that makes those objects meaningful. Compilation produces bytes; loading creates the runtime representation; linking verifies, prepares, and resolves relationships as required; initialization runs the class’s static logic before relevant active use proceeds.
The next planned lesson opens the .class file itself: what the constant pool contains, how bytecode names its dependencies, and how the runtime constant pool connects that representation to interpretation and later JIT compilation.
Sources and implementation boundaries
Adapted from Daily JVM Byte #10 and its reviewed draft. Diagrams are conceptual. Lifecycle rules come from Java SE 25; Metaspace and mirror placement describe HotSpot. The small initialization experiment was executed; leak diagrams describe reasoning examples, not measured leak runs.
- JVMS 5: loading, identity, linking, resolution, and initialization
- JVMS 4.7.2: ConstantValue assignment before clinit
- JLS 12: active use, initialization order, and unloading
- JDK 25 Class API: forName and hidden-class lifetime
- OpenJDK HotSpot: Class mirrors and static-field storage
- OpenJDK Elastic Metaspace review: loader-associated metadata arenas