Class Loading in Java

Classes are found, verified, prepared and initialised on first use, by a hierarchy of loaders that delegate upwards.

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 assignments

Loading 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 initialisation
start
notes
Config initialised
0
A 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 reloading
System.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

ErrorMeaning
ClassNotFoundExceptionA lookup by name failed; checked, usually from reflection
NoClassDefFoundErrorPresent at compile time, missing at runtime, or its initialiser failed earlier
ExceptionInInitializerErrorA static initialiser threw
UnsupportedClassVersionErrorCompiled by a newer JDK than the runtime
LinkageErrorTwo 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 unusable

The 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 loadClass and 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 ClassNotFoundException with NoClassDefFoundError.

Best practices

  • Keep static initialisers small and incapable of failing.
  • Prefer explicit initialisation to relying on class loading order.
  • Override findClass when 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

  1. Add a static block to a class and show that reading a static final String constant does not run it.
  2. Explain the difference between ClassNotFoundException and NoClassDefFoundError.
  3. Why can a class on the classpath never replace java.lang.String?
  4. Write a loader that reads classes from a folder and load one with it.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.