Skip to content

Java Class Loader

The class loader is responsible for loading class data from bytecode files into the JVM, performing verification, parsing, and initialization of the data, ultimately forming Java types that can be directly used by the JVM.

In general web development, we rarely use class loaders directly because web containers shield us from their complexity - we only need to focus on implementing business logic. However, if you’ve developed Java middleware, you’ll find class loaders are used very frequently.

This chapter first introduces the ClassLoader API and its usage, then sequentially covers the ClassLoader source code, JDK and web middleware class loaders, and finally explains the implementation principles of hot loading technology.

4.1 ClassLoader API

ClassLoader is an abstract class that cannot be used directly, so we need to extend it and override its methods. Its main methods include defineClass, loadClass, findClass, resolveClass and their overloaded versions. The key method definitions are as follows:

// Takes a byte array of bytecode as input and outputs a Class object - its purpose is to parse the byte array into a Class object
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
// Finds a Class object by class name
public Class<?> loadClass(String name)
// Locates a class by name
protected Class<?> findClass(String name)
// Called after class loading to complete class linking
protected final void resolveClass(Class<?> c)

Let’s first implement a simple NetworkClassLoader that has the capability to load class files from the network. The implementation code is as follows:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class NetworkClassLoader extends ClassLoader {
// Download URL
private String downloadUrl;
public NetworkClassLoader(String downloadUrl) {
this.downloadUrl = downloadUrl;
}```java
// Implementing the class lookup method
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// Downloads the class file from remote to obtain the bytecode array
private byte[] loadClassData(String name) {
// load the class data from the connection
// ...
}
// Converts class name to server download path
private String classNameToPath(String name) {
return downloadUrl + "/" + name.replace(".", "/") + ".class";
}
// Test method
public class Main {
public static void main(String[] args) throws Exception {
// Download URL
String baseUrl = "https://wwww.jrasp.com";
// Initialize network class loader
NetworkClassLoader loader = new NetworkClassLoader(baseUrl);
// Load class located at https://wwww.jrasp.com/Foo.class and create instance
Object foo = loader.loadClass("Foo").newInstance();
}
}
}

The loaded class Foo is a simple class that outputs “create new instance” when creating an instance object. The code for class Foo is as follows:

public class Foo {
public Foo() {
System.out.println("create new instance");
}
}
// Running the Main method produces the following output:
// create new instance

The main functions of ClassLoader are class lookup, loading and linking. In addition to loading classes, class loaders are also responsible for loading resources such as configuration files or images.

4.2 ClassLoader Source Code Analysis

With the above usage foundation, let’s analyze the source code of the class loader and its important implementation classes.

4.2.1 loadClass

ClassLoader calls its loadClass method to load classes. The core code of loadClass is as follows:

Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // If the parent class loader is not null, attempt to load from parent c = parent.loadClass(name, false); } else { // If parent loader is null, use bootstrap class loader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // Ignore exception and continue searching } if (c == null) { // If parent loaders fail, call the overridden findClass method of current ClassLoader c = findClass(name); } } // Link the class if (resolve) { resolveClass(c); } return c; } }

The class loading sequence above can be summarized as: first attempt to load via parent loader (if parent loader is null, use the system bootstrap class loader), and only when all parent loaders fail will it delegate to the current ClassLoader's overridden findClass method. As shown in Figure 4-1:
> Figure 4-1 Class Loader Delegation Model
![Figure 4-1 Class Loader Delegation Model.png](4-1.png)
### 4.2.2 findClass
During class loading, if all parent loaders fail to find the class, the overridden findClass method of the child class loader will be invoked. The findClass method is as follows:
> Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Throws exception when called
throw new ClassNotFoundException(name);
}

As we can see, this method throws an exception, so it cannot be called directly and requires subclass implementation. URLClassLoader is a subclass of ClassLoader and overrides the findClass method. The properties and constructor of URLClassLoader are as follows:

Code location: src/java.base/share/classes/java/net/URLClassLoader.java

// Search path for classes and resources
private final URLClassPath ucp;
public URLClassLoader(URL[] urls, ClassLoader parent) {
// Specify parent class loader
super(parent);
// ... Permission check code omitted
this.acc = AccessController.getContext();
// Initialize ucp property
ucp = new URLClassPath(urls, acc);
}

Implements the findClass method of ClassLoader to load classes from specified paths.

Code location: src/java.base/share/classes/java/net/URLClassLoader.java

protected Class<?> findClass(final String name) throws ClassNotFoundException {
// 1. Convert fully qualified class name to .class file path format
String path = name.replace('.', '/').concat(".class");
// 2. Check if it exists in URLClassPath
Resource res = ucp.getResource(path, false);
// ... Exception handling omitted
return defineClass(name, res);
}

The execution logic of URLClassLoader’s findClass method mainly consists of three steps:

  • Convert the fully qualified class name to .class file path format;
  • Check if the file exists in URLs;
  • Call defineClass to complete class linking and initialization;

4.2.3 defineClass

defineClass is used together with findClass. findClass is responsible for reading bytecode from disk or network, while defineClass parses the bytecode into a Class object. The defineClass method uses resolveClass to complete the linking of the Class. The source code is as follows:

Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java protected final Class defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); // Call native method to complete linking Class c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }

The implementation of `defineClass` lies in the `defineClass1` method, which is a native method with its specific implementation in HotSpot. The implementation is quite complex and generally doesn't require special attention.
The steps required when a ClassLoader loads a class file into the JVM are shown in Figure 4-2 below:
> Figure 4-2 Phases of JVM Class Loading
![Figure 4-2 Phases of JVM Class Loading](4-2.png)
Typically, we only need to override the `findClass` method of ClassLoader to obtain the bytecode of the class to be loaded, then call the `defineClass` method to generate the Class object. If you want the class to be linked immediately when loaded into the JVM, you can call the `resolveClass` method, or leave it to the JVM to link during class initialization.
## 4.3 JDK's Class Loaders
JDK's own jar packages such as rt.jar and tools.jar (or modules in JDK9+) also require class loaders for loading. The following code demonstrates how to obtain JDK's built-in class loaders:
```java
public class JdkClassloader {
public static void main(String[] args) {
// Get system class loader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// Get parent of system class loader --> extension class loader or platform class loader
ClassLoader platformClassLoader = systemClassLoader.getParent();
System.out.println(platformClassLoader);
// Get parent of extension class loader --> bootstrap class loader (C/C++)
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println(bootstrapClassLoader);
}
}
```Running on JDK8:

sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4a574795 null

Running on JDK11:

jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17 jdk.internal.loader.ClassLoaders$PlatformClassLoader@3cda1055 null

As shown, there are differences in the class names of class loaders between JDK8 and JDK11. The implementations are explained separately below.
### 4.3.1 JDK8 Class Loaders
#### 4.3.1.1 AppClassLoader
AppClassLoader, also known as System ClassLoader, extends URLClassLoader. It is one of the default class loaders in the JVM, primarily used to load user classes and third-party dependency packages. The loading path can be specified via the `-Djava.class.path` parameter in the JVM startup command.
> Code location: src/share/classes/sun/misc/Launcher$AppClassLoader.java
```java
// AppClassLoader extends URLClassLoader
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException {
// Search path java.class.path
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}
// Overrides loadClass
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// Calls parent URLClassLoader to complete class loading
return (super.loadClass(name, resolve));
}
// Other methods omitted...
}

4.3.1.2 ExtClassLoader

The ExtClassLoader, known as the Extension ClassLoader, inherits from URLClassLoader and is primarily responsible for loading Java’s extension libraries. By default, it loads all JAR packages in the ${JAVA_HOME}/jre/lib/ext/ directory. Its search path can also be configured using the -Djava.ext.dirs parameter.

Code location: src/share/classes/sun/misc/Launcher$ExtClassLoader.java

// ExtClassLoader inherits from URLClassLoader
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException {
final File[] dirs = getExtDirs();
try {
return new ExtClassLoader(dirs);
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
private static File[] getExtDirs() {
// Specify the loading path through system properties
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
}

The inheritance relationship of class loaders in JDK8 is shown in Figure 4-3 below:

Figure 4-3: Inheritance Relationship of Class Loaders in JDK8

Figure 4-3: Inheritance Relationship of Class Loaders in JDK8

4.3.1.3 Initialization of Class Loaders in JDK8

The initialization of JDK’s class loaders is implemented in the Launcher class.

Source location: src/share/classes/sun/misc/Launcher.java

public class Launcher {```
public Launcher() {
// Create ExtClassLoader
ClassLoader extcl = ExtClassLoader.getExtClassLoader();
// Create AppClassLoader
ClassLoader loader = AppClassLoader.getAppClassLoader(extcl);
// Set current thread's ContextClassLoader
Thread.currentThread().setContextClassLoader(loader);
// Exception handling code omitted
}
// ...
}

As can be seen, the initialization process is relatively simple: first initialize the ExtClassLoader, then initialize the AppClassLoader, and set the AppClassLoader’s parent loader as ExtClassLoader.

4.3.2 Class Loaders in JDK11

After implementing modularization in JDK9, some modifications were made to Classloader, one of which was changing ExtClassLoader to PlatformClassLoader. With modularization, different Classloaders load their corresponding modules. Since JDK11 is a long-term supported stable version, we use JDK11’s source code to illustrate the changes in class loaders. The inheritance relationship of class loaders in JDK11 is shown in Figure 4-4 below:

Figure 4-4 Inheritance relationship of class loaders in JDK11

Figure 4-4 Inheritance relationship of class loaders in JDK11

4.3.2.1 BuiltinClassLoader

BuiltinClassLoader is the parent class of PlatformClassLoader, BootClassLoader, and AppClassloader. Functionally similar to URLClassLoader, it implements class lookup based on UrlClassPath, but BuiltinClassLoader also supports loading classes from modules.

The properties and constructor of BuiltinClassLoader are as follows:

Code location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

// Class loader path
private final URLClassPath ucp;
BuiltinClassLoader(String name, BuiltinClassLoader parent, URLClassPath ucp) {
// Ensure returning null when parent loader is bootloader
// name is the name of the class loader
super(name, parent == null || parent == ClassLoaders.bootLoader() ? null : parent);
this.parent = parent;
this.ucp = ucp;
this.nameToModule = new ConcurrentHashMap<>();
this.moduleToReader = new ConcurrentHashMap<>();
}
```The `BuiltinClassLoader` also overrides the `loadClass` method, which actually calls the `loadClassOrNull` method. Let's examine the implementation of the `loadClassOrNull` method.
> Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
```java
protected Class<?> loadClassOrNull(String cn, boolean resolve) {
// Lock to ensure thread safety
synchronized (getClassLoadingLock(cn)) {
// First check if the class has already been loaded (this is a native method in ClassLoader)
Class<?> c = findLoadedClass(cn);
if (c == null) {
// Need to load module information first
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
BuiltinClassLoader loader = loadedModule.loader();
if (loader == this) {
if (VM.isModuleSystemInited()) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// Delegate to another class loader
c = loader.loadClassOrNull(cn);
}
} else {
// First try to load using the parent loader
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
// If still not loaded, use the current loader
if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
// This method internally calls defineClass to complete class definition
c = findClassOnClassPathOrNull(cn);
}
}
}
if (resolve && c != null)
resolveClass(c);```
return c;
}
}

There is a slight difference from the usual parent delegation. If a class belongs to a module, the module’s class loader will be directly invoked to load it, rather than using the parent delegation model of the current class loader. However, once the corresponding class loader for this class is found, parent delegation will still be followed for the loading process.

BuiltinClassLoader also overrides the findClass method of ClassLoader.

Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

@Override
protected Class<?> findClass(String cn) throws ClassNotFoundException {
// Attempt to find in modules
LoadedModule loadedModule = findLoadedModule(cn);
Class<?> c = null;
if (loadedModule != null) {
// Delegate loading task to the module's loader
if (loadedModule.loader() == this) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// Search in classpath
if (hasClassPath()) {
c = findClassOnClassPathOrNull(cn);
}
}
// If not found, throw exception
if (c == null)
throw new ClassNotFoundException(cn);
return c;
}

Here, findClassOnClassPathOrNull searches for the class in the classpath.

Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

private Class<?> findClassOnClassPathOrNull(String cn) {
String path = cn.replace('.', '/').concat(".class");
// Permission check code omitted...
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(cn, res);
} catch (IOException ioe) {
// TBD on how I/O errors should be propagated
}
}
return null;
}
4.3.2.2 Subclasses of BuiltinClassLoader and Initialization

The ClassLoaders class initializes the BootClassLoader, PlatformClassLoader, and AppClassLoader class loaders respectively.> Source location: src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java

public class ClassLoaders {
// JDK built-in class loaders
private static final BootClassLoader BOOT_LOADER;
private static final PlatformClassLoader PLATFORM_LOADER;
private static final AppClassLoader APP_LOADER;
// Initialize class loader objects
static {
// Can be specified using -Xbootclasspath/a or Boot-Class-Path attribute in -javaagent
String append = VM.getSavedProperty("jdk.boot.class.path.append");
// Initialize BOOT_LOADER
BOOT_LOADER =
new BootClassLoader((append != null && append.length() > 0)
? new URLClassPath(append, true)
: null);
// Initialize PLATFORM_LOADER and set BOOT_LOADER as parent of AppClassLoader
PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
// Get classpath
String cp = System.getProperty("java.class.path");
if (cp == null || cp.length() == 0) {
String initialModuleName = System.getProperty("jdk.module.main");
cp = (initialModuleName == null) ? "" : null;
}
URLClassPath ucp = new URLClassPath(cp, false);
// Initialize APP_LOADER and set PLATFORM_LOADER as parent of AppClassLoader
APP_LOADER = new AppClassLoader(PLATFORM_LOADER, ucp);
}
// ...
}

From the initialization code of the class loader instances, we can see that BootClassLoader is used to load classes specified by the jdk.boot.class.path.append parameter. During PLATFORM_LOADER initialization, BOOT_LOADER is set as its parent, and during AppClassLoader initialization, PLATFORM_LOADER is set as its parent, forming a three-tier class loader structure.

Now let’s look at the PlatformClassLoader class which is specific to JDK9 and above:```java private static class PlatformClassLoader extends BuiltinClassLoader {

PlatformClassLoader(BootClassLoader parent) {
// The class loader name is "platform"
super("platform", parent, null);
}
// ...

}

Different class loaders are responsible for loading corresponding modules, which are specified during JDK compilation.
> Source: jdk11-1ddf9a99e4ad/make/common/Modules.gmk
+ BOOT_MODULES are modules defined by the bootstrap loader:
```text
java.base java.datatransfer
java.desktop java.instrument
java.logging java.management
java.management.rmi java.naming
java.prefs java.rmi
java.security.sasl java.xml
jdk.internal.vm.ci jdk.jfr
jdk.management jdk.management.jfr
jdk.management.agent jdk.net
jdk.sctp jdk.unsupported
jdk.naming.rmi
  • PLATFORM_MODULES are modules defined by the platform loader:
java.net.http java.scripting
java.security.jgss java.smartcardio
java.sql java.sql.rowset
java.transaction.xa java.xml.crypto
jdk.accessibility jdk.charsets
jdk.crypto.cryptoki jdk.crypto.ec
jdk.dynalink jdk.httpserver
jdk.jsobject jdk.localedata
jdk.naming.dns jdk.scripting.nashorn
jdk.security.auth jdk.security.jgss
jdk.xml.dom jdk.zipfs
jdk.crypto.mscapi jdk.crypto.ucrypto
java.compiler jdk.aot
jdk.internal.vm.compiler
jdk.internal.vm.compiler.management
java.se
  • JRE_TOOL_MODULES are tools included in JRE, loaded by AppClassLoader:
jdk.jdwp.agent
jdk.pack
jdk.scripting.nashorn.shell

Other modules not listed are loaded by AppClassLoader.## 4.4 Web Container Class Loaders

The previous section introduced the general class loading model in Java: the Parent Delegation Model, which applies to most class loading scenarios. However, this model is not suitable for web containers because the Servlet specification imposes certain requirements on web container class loading. The main rules are as follows:

  • Classes under WEB-INF/classes and WEB-INF/lib take precedence over classes from the parent container. For example, if there is a Foo class in WEB-INF/classes and another Foo class in CLASSPATH, the web container loader will prioritize loading the class from WEB-INF/classes. This behavior is contrary to the Parent Delegation Model.
  • System classes like java.lang.Object do not follow the first rule. Classes in WEB-INF/classes or WEB-INF/lib cannot replace system classes. The specification does not explicitly define which classes are considered system classes, but web containers typically determine them by enumerating certain classes.
  • The implementation classes of the web container itself cannot be referenced by application classes, meaning no application class loader can load the web container’s implementation classes. The web container identifies its own classes by enumerating package names.

4.4.1 Jetty Class Loader

To meet these three requirements and achieve dependency isolation between different deployed applications, Jetty defines its own class loader called WebAppClassLoader. The inheritance relationship of this class loader is shown below:

Figure 4-5 Inheritance Relationship of Jetty Class Loaders

Figure 4-5 Inheritance Relationship of Jetty Class Loaders

The attributes of WebAppClassLoader are as follows:

// Class loader context
private final Context _context;
// Parent loader
private final ClassLoader _parent;
// File extensions to load (.zip or .jar)
private final Set<String> _extensions = new HashSet<String>();
// Loader name
private String _name = String.valueOf(hashCode());
// Pre-class-loading transformers
private final List<ClassFileTransformer> _transformers = new CopyOnWriteArrayList<>();

Classes whose package path names are included in the following paths are considered system classes. System classes are visible to application classes.

// System classes cannot be replaced by classes from application JARs and can only be loaded by the system classloader
public static final ClassMatcher __dftSystemClasses = new ClassMatcher(
"java.","javax.","org.xml.","org.w3c."
);

Server classes are not visible to any application. Jetty also uses package path names to identify Server classes. The configuration in WebAppContext is as follows:

// Loaded by the system classloader and invisible to web applications
public static final ClassMatcher __dftServerClasses = new ClassMatcher(
"org.eclipse.jetty."
);
```We can set Server classes using the `WebAppContext.addServerClasses` or `WebAppContext.addServerClassMatcher` methods. Note that Server classes are invisible to all applications, but application classes under WEB-INF/lib can replace Server classes.
> Code location: jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
```java
public static void addServerClasses(Server server, String... pattern) {
addClasses(__dftServerClasses, SERVER_SRV_CLASSES, server, pattern);
}
public static void addSystemClasses(Server server, String... pattern) {
addClasses(__dftSystemClasses, SERVER_SYS_CLASSES, server, pattern);
}
public void addServerClassMatcher(ClassMatcher serverClasses) {
_serverClasses.add(serverClasses.getPatterns());
}
public void addSystemClassMatcher(ClassMatcher systemClasses) {
_systemClasses.add(systemClasses.getPatterns());
}
```The constructor of WebAppClassLoader is as follows:
```java
public WebAppClassLoader(ClassLoader parent, Context context)
throws IOException {
// Specify parent class loader
super(new URL[]{}, parent != null ? parent
: (Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader()
: (WebAppClassLoader.class.getClassLoader() != null ? WebAppClassLoader.class.getClassLoader()
: ClassLoader.getSystemClassLoader())));
_parent = getParent();
_context = context;
if (_parent == null)
throw new IllegalArgumentException("no parent classloader!");
// File types that the class loader can load: jar or zip packages
_extensions.add(".jar");
_extensions.add(".zip");
}

The constructor can explicitly specify a parent class loader. By default, it is null, meaning the current thread’s context classLoader is designated as the parent. If not specified by the user, this thread context classLoader will default to the previously mentioned System ClassLoader.

Now let’s look at the loadClass method.

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
ClassNotFoundException ex = null;
Class<?> parentClass = null; // From parent class loader
Class<?> webappClass = null; // From webapp class loader```
// First search from already loaded classes
webappClass = findLoadedClass(name);
if (webappClass != null) {
return webappClass;
}
// First try loading from current class loader (true here indicates checking if the class is a system class, if not, return the loaded class)
webappClass = loadAsResource(name, true);
if (webappClass != null) {
return webappClass;
}
// Then try loading from parent class loader
try {
parentClass = _parent.loadClass(name);
// Check if loading server classes is allowed, or if the current class is not a server class
if (Boolean.TRUE.equals(__loadServerClasses.get())
|| !_context.isServerClass(parentClass)) {
return parentClass;
}
} catch (ClassNotFoundException e) {
ex = e;
}
// Try loading from current class loader again (false here indicates not checking if the class is a system class)
webappClass = loadAsResource(name, false);
if (webappClass != null) {
return webappClass;
}
throw ex == null ? new ClassNotFoundException(name) : ex;
}
}

4.4.2 Tomcat Class Loader

Similar to the Jetty container, Tomcat also needs to comply with the three servlet specifications. The inheritance relationship of Tomcat’s class loaders is shown in Figure 4-6 below.

Figure 4-6 Inheritance Relationship of Tomcat Class Loaders

Figure 4-6 Inheritance Relationship of Tomcat Class Loaders

4.4.2.1 WebappClassLoader> Source: apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappLoader.java

public class WebappClassLoader extends WebappClassLoaderBase {
public WebappClassLoader() {
super();
}
public WebappClassLoader(ClassLoader parent) {
super(parent);
}
//...
}

WebappClassLoader extends WebappClassLoaderBase, with the core class loading functionality primarily implemented in WebappClassLoaderBase. Looking directly at the code of WebappClassLoaderBase, it is an abstract class that inherits from URLClassLoader and overrides the loadClass method.

Source: apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java

First, let’s examine its properties and constructor:

// Whether to use the parent delegation model
protected boolean delegate = false;
// Class loader for loading JavaSE classes
private ClassLoader javaseClassLoader;
// Parent loader of the current class loader
protected final ClassLoader parent;
protected WebappClassLoaderBase() {
super(new URL[0]);
// If no parent loader is specified during initialization, the parent defaults to the system class loader
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
// Initialize javaseClassLoader as the platform class loader or extension class loader
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.javaseClassLoader = j;
}

The loadClass method is overridden as follows:

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass0(name);
if (clazz != null) return clazz;```
// If not found in the web application's local class cache, search in the system class loader cache,
// If found, it means AppClassLoader has already loaded this class before
clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// Convert class names like java.lang.String to java/lang/String format
String resourceName = binaryNameToPath(name, false);
// Get the bootstrap class loader (BootstrapClassLoader)
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// The bootstrap class loader gets the resource URL based on the converted class name
// If the URL is not null, it means the class to be loaded is found
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// ...
}
// First, try loading from the extension class loader (ExtClassLoader)
if (tryLoadingFromJavaseLoader) {
return javaseLoader.loadClass(name);
}
// delegate allows class loading to be delegated to the parent class loader
boolean delegateLoad = delegate || filter(name, true);
if (delegateLoad) {
return Class.forName(name, false, parent);
}
// Load from the current web path
clazz = findClass(name);
// If the class still hasn't been loaded after the above steps,
// use the system class loader (also known as the application class loader) to load it
if (!delegateLoad) {
return Class.forName(name, false, parent);
}
}
// Finally, if the class still hasn't been loaded, throw ClassNotFoundException
throw new ClassNotFoundException(name);
}

4.2.2.3 JSP Class Loader

The JSP class loader also inherits from URLClassLoader and overrides loadClass. Let’s examine its source code.

Source: apache-tomcat-10.1.13-src/java/org/apache/jasper/servlet/JasperLoader.java```java public class JasperLoader extends URLClassLoader {

@Override
public synchronized Class<?> loadClass(final String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
// Look up in JVM class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// SecurityManager code omitted
// If class name doesn't start with org.apache.jsp package, use WebappClassLoader to load
if( !name.startsWith(Constants.JSP_PACKAGE_NAME + '.') ) {
clazz = getParent().loadClass(name);
if( resolve ) {
resolveClass(clazz);
}
return clazz;
}
// For classes starting with org.apache.jsp package, call URLClassLoader's findClass method
// to dynamically load class files, parse into Class objects, and return to caller
return findClass(name);
}

}

From the source code, we can see that JSP class loading first attempts to load from the JVM class cache (classes loaded by Bootstrap and other class loaders). If it's not a JSP class, it loads from the web application class loader WebappClassLoader. If still not found, it loads from the specified URL path.
The initialization code for JasperLoader is as follows:
> Source: apache-tomcat-10.1.13-src/java/org/apache/jasper/JspCompilationContext.java
```java
public ClassLoader getJspLoader() {
if( jspLoader == null ) {
jspLoader = new JasperLoader(new URL[] {baseUrl}, getClassLoader(),
basePackageName, rctxt.getPermissionCollection());
}
return jspLoader;
}

When initializing JasperLoader, the loading path and parent loader are specified.## 4.5 Thread Context Class Loader

In the previous sections, we focused on analyzing the implementation principles of the parent delegation model and arrived at a fundamental conclusion: child class loaders can use classes already loaded by parent class loaders, whereas parent class loaders cannot use classes loaded by child class loaders. This leads to situations where the parent delegation model cannot solve all class loader issues. For example, Java provides certain interfaces (Service Provider Interface, SPI) such as JDBC, JNDI, and JAXP. These interface classes are loaded by either the BootstrapClassLoader or PlatformClassLoader, but their implementations are typically provided by third parties and loaded by the AppClassLoader. The BootstrapClassLoader cannot load the implementation classes of these core interfaces because it only loads Java’s core libraries. It also cannot delegate to the AppClassLoader since it is the topmost class loader. In other words, the parent delegation model cannot resolve this issue. To address this problem, Java introduced the thread context class loader (ContextClassLoader).

The relevant content about the thread context class loader is found in the Thread class, as shown in the following code:

jdk11/src/java.base/share/classes/java/lang/Thread.java

public class Thread implements Runnable {
// Other properties omitted...
private ClassLoader contextClassLoader;
// Get the context class loader
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
return contextClassLoader;
}
// Set the context class loader
public void setContextClassLoader(ClassLoader cl) {
contextClassLoader = cl;
}
// Create a thread instance
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// The thread creating this thread instance
Thread parent = currentThread();
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();```
// Initialize to parent thread's context class loader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// Other code omitted.......
}
}

From the source code, we can see that the thread context class loader is an attribute of the Thread class. It can cache the class loader used by the current thread, inherits the parent thread’s context class loader when the thread is created, and can be set to other values during thread execution.

In JDBC, DriverManager is used to manage different database drivers introduced in a project, such as MySQL drivers and Oracle drivers. In JDK 11, the DriverManager class is in the java.sql module, which is loaded by the PlatformClassLoader, while the MySQL driver classes in dependencies are loaded by the application class loader. Under the parent delegation model, DriverManager cannot access MySQL driver classes. Here, JDK uses the context class loader to bypass this restriction.

Let’s look at the DriverManager source code implementation:

jdk11/src/java.sql/share/classes/java/sql/DriverManager.java

public static Driver getDriver(String url) throws SQLException {
// Load driver class
ensureDriversInitialized();
for (DriverInfo aDriver : registeredDrivers) {
try {
if (aDriver.driver.acceptsURL(url)) {
return (aDriver.driver); // Get driver
}
} catch(SQLException sqe) {
// ignore
}
}
throw new SQLException("No suitable driver", "08001");
}

From its name, ensureDriversInitialized ensures that driver classes are properly initialized, then iterates through registered drivers and returns the Driver object. Let’s first examine how the ensureDriversInitialized method obtains driver classes.

private static void ensureDriversInitialized() {
if (driversInitialized) return;```java
synchronized (lockForInitDrivers) {
if (driversInitialized) return;
String drivers;
// Get driver class names from system environment variables
try {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
} catch (Exception ex) {
drivers = null;
}
// Use ServiceLoader to read driver implementations from jar packages
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
// Class loading
if (drivers != null && !drivers.equals("")) {
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
// Attempt to load the class here. If SystemClassLoader fails, driver initialization fails
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
// ...
}
}
}
driversInitialized = true;
}
}

The loading of driver classes mainly occurs in ServiceLoader.load(Driver.class). The implementation of the load method is as follows:

jdk11/src/java.base/share/classes/java/util/ServiceLoader.java

public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

From the source code, we can see that the current thread’s context class loader is used when loading classes.

4.6 Hot Loading and Unloading

During the class loading process, we know that the JVM first checks whether the class has already been loaded. If it has, it won’t look for the class in jar packages or paths but will use the cached class instead. The JVM determines whether two classes are the same based on two conditions: first, whether their fully qualified names are identical, and second, whether their class loader instances are the same. Therefore, to achieve hot loading of classes, different class loaders can be used to load the same class file. Using different class loader instances to load the same class file will result in an increasing number of class instances as loading occurs repeatedly. If metaspace/permanent generation is not cleaned up promptly, there is a risk of memory overflow.

However, the conditions for class unloading are very stringent. Generally, all three of the following conditions must be met simultaneously, and a full GC must be performed by the JVM before complete cleanup can occur. The three conditions for class unloading are as follows:

Figure 4-7 Conditions for Class Unloading

Figure 4-7 Conditions for Class Unloading

The timing of full GC is beyond our control, and consequently, so is class unloading. From the three conditions above, we can see that the JVM’s built-in class loaders cannot be reclaimed, meaning classes loaded by the JVM will not be unloaded. Only custom class loaders have the potential to be unloaded. Below is a specific requirement implemented using hot loading: an application loads a class script at runtime, and the script can be hot-updated. There is a script interface with computation execution functionality.

public interface Script {
// Execute computation
String run(String key);
}

The implementation class of the script is responsible for specific computation functionality.

public class ScriptImpl implements Script {
public ScriptImpl() {
}
public String run(String key) {
return key;
}
}

Replacing the script implementation during JVM runtime enables script updates.

public class Main {
public static void main(String[] args) throws Exception {
ClassLoader appClassloader = Main.class.getClassLoader();
ScriptClassLoader scriptClassLoader1 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl1 = scriptClassLoader1.loadClass("ScriptImpl");
System.out.println(scriptImpl1.hashCode());```
ScriptClassLoader scriptClassLoader2 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl2 = scriptClassLoader2.loadClass("ScriptImpl");
// The class objects are not identical
assert scriptImpl1 != scriptImpl2;
}
}

When loading the same class using different class loaders, the resulting class objects will be different. This allows for runtime updates to the implementation of the ScriptImpl class. The implementation of ScriptClassLoader is as follows:

public class ScriptClassLoader extends ClassLoader {
private String classDir;
public ScriptClassLoader(String classDir,ClassLoader classLoader) {
super(classLoader);
this.classDir = classDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classDate = getClassByte(name);
if (classDate == null) {
return null;
}
return defineClass(name, classDate, 0, classDate.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}```java
private byte[] getClassByte(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classDir + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
in.close();
out.close();
}
return null;
}
}