Class Loading in Java
Classes are found, verified, prepared and initialised on first use, by a hierarchy of loaders that delegate upwards.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The three phases
Loading find the .class bytes and create a Class object
|
Linking verify - check the bytecode is well formed and safe
| prepare - allocate static fields with default values
| resolve - turn symbolic references into direct ones
|
Initialising run static initialisers and static field assignmentsLoading is lazy
public class Config {
static {
System.out.println("Config initialised");
}
public static final String NAME = "notes";
public static int counter = 0;
}System.out.println("start");
System.out.println(Config.NAME); // a compile time constant: NOT initialised
System.out.println(Config.counter); // a real field read: triggers initialisationstart
notes
Config initialised
0A static final field with a constant initialiser is inlined by the compiler, so reading it does not load the class. Any other static access does. This catches people who expect a static block to run and find that it never does.What triggers initialisation
- Creating an instance with
new. - Reading or writing a static field that is not a compile time constant.
- Calling a static method.
- Reflection such as
Class.forName("..."). - Initialising a subclass, which initialises the superclass first.
The loader hierarchy
Bootstrap class loader core platform classes, java.lang and so on
^
Platform class loader other platform modules
^
Application class loader your classes, from the classpath or module path
^
Custom loaders plugins, containers, hot reloadingSystem.out.println(String.class.getClassLoader()); // null: bootstrap
System.out.println(Config.class.getClassLoader()); // app class loader
System.out.println(ClassLoader.getSystemClassLoader());The bootstrap loader is part of the JVM itself and is reported as null, which is expected rather than an error.
Parent delegation
request for java.lang.String
application loader -> ask parent
platform loader -> ask parent
bootstrap -> found, loaded
A loader always asks its parent before trying itself. This is a security property: a class named java.lang.String placed on the classpath can never replace the real one, because the bootstrap loader answers first. The JVM also rejects any user defined class in a java. package outright.
Class identity includes its loader
// The same bytes loaded by two different loaders produce two different classes
Object a = loaderOne.loadClass("com.example.Note").getDeclaredConstructor().newInstance();
Object b = loaderTwo.loadClass("com.example.Note").getDeclaredConstructor().newInstance();
// ClassCastException, although the name is identical
// com.example.Note note = (com.example.Note) b;A class is identified by its fully qualified name and its defining loader. This is what allows a container to isolate applications, and it is also the source of the confusing message that a class cannot be cast to itself.
A custom class loader
public class DirectoryClassLoader extends ClassLoader {
private final Path root;
public DirectoryClassLoader(Path root, ClassLoader parent) {
super(parent);
this.root = root;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path file = root.resolve(name.replace(".", "/") + ".class");
try {
byte[] bytes = Files.readAllBytes(file);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}Override findClass, not loadClass. The inherited loadClass already implements parent delegation, and replacing it usually breaks it.
Static initialisation order
public class Order {
static int first = report("1 static field");
static {
System.out.println("2 static block");
}
static int second = report("3 second static field");
private static int report(String message) {
System.out.println(message);
return 0;
}
}Static fields and static blocks run in source order, once, when the class is initialised. The JVM guarantees this happens exactly once even with many threads, which is what makes the static holder singleton idiom safe.
Common errors
| Error | Meaning |
|---|---|
ClassNotFoundException | A lookup by name failed; checked, usually from reflection |
NoClassDefFoundError | Present at compile time, missing at runtime, or its initialiser failed earlier |
ExceptionInInitializerError | A static initialiser threw |
UnsupportedClassVersionError | Compiled by a newer JDK than the runtime |
LinkageError | Two incompatible versions of a class are present |
public class Broken {
static final Map<String, String> VALUES = load(); // throws
private static Map<String, String> load() {
throw new IllegalStateException("no configuration");
}
}
// First use: ExceptionInInitializerError
// Every later use: NoClassDefFoundError, because the class is marked unusableThe second message is the confusing one. Once initialisation fails, the class stays broken, and later failures no longer mention the original cause. Always look for the first occurrence in the log.
Common mistakes
- Expecting a static block to run merely because the class is imported.
- Overriding
loadClassand breaking parent delegation. - Doing heavy or failure prone work in a static initialiser.
- Assuming two classes with the same name are the same type.
- Confusing
ClassNotFoundExceptionwithNoClassDefFoundError.
Best practices
- Keep static initialisers small and incapable of failing.
- Prefer explicit initialisation to relying on class loading order.
- Override
findClasswhen writing a custom loader. - When diagnosing a linkage problem, look for the earliest error in the log.
- Do not depend on when a class is loaded.
Practice
- Add a static block to a class and show that reading a
static final Stringconstant does not run it. - Explain the difference between
ClassNotFoundExceptionandNoClassDefFoundError. - Why can a class on the classpath never replace
java.lang.String? - Write a loader that reads classes from a folder and load one with it.
- Why does the same class loaded by two loaders fail to cast?
Conclusion
Classes are loaded on first real use, linked, and then initialised exactly once. Delegation upwards protects the platform classes, and a class is identified by its name together with its loader, which explains most puzzling linkage errors.