Understanding Java Class Loading, Linking, and Initialization
This article explains the three phases of Java class loading—loading, linking, and initialization—detailing how the JVM handles class files, the role of different class loaders, and how to implement a custom class loader with practical code examples.
1. Introduction
When a program references a class that has not yet been loaded into memory, the JVM performs three steps—loading, linking, and initialization—to bring the class into the runtime.
2. Class Loading, Linking, and Initialization
2.1 Loading
Loading reads the class file into memory and creates a java.lang.Class object, performed by a class loader provided by the JVM. Developers can also create custom loaders by extending ClassLoader .
2.1.1 Sources of class files
Local file system
JAR packages
Network
Dynamic compilation of Java source files
2.2 Linking
Linking merges the binary data into the JRE and consists of verification, preparation, and resolution.
Verification
Checks the internal structure of the class and ensures it meets JVM constraints.
Preparation
Allocates memory for static variables and assigns default values.
Resolution
Transforms symbolic references (e.g., method signatures) into actual memory addresses; if a referenced class is not loaded, resolution triggers its loading.
2.3 Initialization
The JVM executes the <clinit> method to initialize static variables, either at the point of definition or within static initialization blocks.
public class Test {
static int A = 10;
static {
A = 20;
}
}
class Test1 extends Test {
private static int B = A;
public static void main(String[] args) {
System.out.println(Test1.B);
}
}
// Output: 20Static initialization of a superclass occurs before a subclass’s static variables are initialized.
public interface InterfaceInitTest {
long A = CurrentTime.getTime();
}
interface InterfaceInitTest1 extends InterfaceInitTest {
int B = 100;
}
class InterfaceInitTestImpl implements InterfaceInitTest1 {
public static void main(String[] args) {
System.out.println(InterfaceInitTestImpl.B);
System.out.println("---------------------------");
System.out.println("Current time:" + InterfaceInitTestImpl.A);
}
}
class CurrentTime {
static long getTime() {
System.out.println("Loading InterfaceInitTest interface");
return System.currentTimeMillis();
}
}
// Output: 100, separator, loading message, current timeInterfaces are initialized only when a static field defined in the interface is actually used.
The JVM guarantees that <clinit> execution is thread‑safe; only one thread runs the method while others block.
public class MultiThreadInitTest {
static int A = 10;
static {
System.out.println(Thread.currentThread() + "init MultiThreadInitTest");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
}
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread() + "start");
System.out.println(MultiThreadInitTest.A);
System.out.println(Thread.currentThread() + "run over");
};
new Thread(r).start();
new Thread(r).start();
}
}
// First thread performs initialization; second thread waits.2.4 Class Loading Timing
When the JVM starts, it initializes the user‑specified main class.
When a new instruction creates an object.
When a static method is invoked or a static field is accessed.
Subclass initialization triggers superclass initialization.
Initialization of an interface occurs when a default method or a static field is used.
Reflective calls via the Reflection API trigger initialization.
Class.forName() forces initialization.
2.5 Final Constants
If a final constant’s value is known at compile time, it behaves like a macro and does not trigger class initialization; otherwise, initialization occurs on first use.
public class StaticInnerSingleton {
private static class LazyHolder {
private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
}
private StaticInnerSingleton() {}
public static StaticInnerSingleton getInstance() { return LazyHolder.INNER_SINGLETON; }
}
// Lazy initialization via static inner class.3. Class Loaders
Class loaders load .class files (from JARs, local disk, or network) and create corresponding Class objects. Once a class is loaded, it is not loaded again.
3.1 JVM Class Loader Types
Bootstrap ClassLoader
Loads core Java libraries from %JAVA_HOME%/jre/lib . It is implemented in native code, not a subclass of ClassLoader .
public class BootstrapTest {
public static void main(String[] args) {
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urls).forEach(System.out::println);
}
}
// Prints paths of core JARs.Extension ClassLoader
Loads JARs from %JAVA_HOME%/jre/ext or directories specified by java.ext.dirs .
System (Application) ClassLoader
Loads classes from the classpath specified by the -classpath option, java.class.path property, or the CLASSPATH environment variable. User‑defined loaders typically have this as their parent.
public class ClassloaderPropTest {
public static void main(String[] args) throws IOException {
ClassLoader system = ClassLoader.getSystemClassLoader();
System.out.println("System ClassLoader: " + system);
Enumeration
resources = system.getResources("");
while (resources.hasMoreElements()) System.out.println(resources.nextElement());
ClassLoader ext = system.getParent();
System.out.println("Parent (Extension) ClassLoader: " + ext);
System.out.println("Extension dirs: " + System.getProperty("java.ext.dirs"));
System.out.println("Extension parent: " + ext.getParent());
}
}
// Demonstrates hierarchy: System -> Extension -> Bootstrap (null).3.2 Class Loading Mechanism
Full delegation: a loader loads a class and all its dependencies.
Parent‑delegation (double‑parent delegation): the parent loader attempts loading first; only if it fails does the child loader try.
Cache: already loaded classes are cached; changes to class files require JVM restart.
4. Custom Class Loader
All non‑bootstrap loaders extend ClassLoader . By overriding findClass (instead of loadClass ), developers can define custom loading logic.
public class MyClassloader extends ClassLoader {
private byte[] getBytes(String fileName) throws IOException {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file)) {
int read = fin.read(raw);
if (read != len) throw new IOException("Unable to read full file");
return raw;
}
}
@Override
protected Class
findClass(String name) throws ClassNotFoundException {
String fileStub = name.replace(".", "/");
String classFileName = fileStub + ".class";
File classFile = new File(classFileName);
if (classFile.exists()) {
try {
byte[] raw = getBytes(classFileName);
return defineClass(name, raw, 0, raw.length);
} catch (IOException e) { e.printStackTrace(); }
}
throw new ClassNotFoundException(name);
}
public static void main(String[] args) throws Exception {
String classPath = "loader.Hello";
MyClassloader loader = new MyClassloader();
Class
cls = loader.loadClass(classPath);
Method m = cls.getMethod("test", String.class);
System.out.println(m);
m.invoke(cls.newInstance(), "Hello World");
}
}
// Loads and invokes a user‑defined class at runtime.The key method defineClass(String name, byte[] b, int off, int len) converts a byte array into a Class object.
5. Summary
The article detailed the three JVM class‑loading phases, explained the differences and relationships among the Bootstrap, Extension, and System class loaders, described the delegation and caching mechanisms, and demonstrated how to create a custom class loader with concrete code examples.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.