This article analyzes the implementation principle of the parent delegate model and illustrates when and how to implement your own class loader with code examples.

This article is based on JDK8.

0 Function of ClassLoader

The ClassLoader is used to load class files into the JVM. Another function is to confirm which class loader each class should be loaded by.

The second effect is also used to determine whether two classes at JVM runtime are equal, affecting equals(), isAssignableFrom(), isInstance(), and the instanceof keyword, as illustrated later.

0.1 when does class loading start?

Triggering of class loading can be divided into implicit loading and display loading.

Implicit load

Implicit loading includes the following cases:

  • When new, getstatic, putstatic, invokestatic are encountered
  • When a reflection call is made to a class
  • When initializing a class, if the parent class has not already been initialized, the parent class is loaded first and initialized
  • When starting a VM, specify a main class that contains the main function, and load and initialize the main class first

According to load

The display load includes the following cases:

  • Through the loadClass method of ClassLoader
  • By Class. Class.forname
  • Through the findClass method of ClassLoader

0.2 Where are loaded classes stored

The JDK8 is previously loaded into the method area in memory. From JDK8 up to now, the metadata area will be loaded.

1 What classloaders are available

The entire JVM platform provides three classes of ClassLoaders.

1.1 the Bootstrap this

Loads the classes that the JVM itself needs to work, which are implemented by the JVM itself. It loads the files under $JAVA_HOME/jre/lib

1.2 ExtClassLoader

It is part of the JVM and is provided by sun.misc.LauncherFiles in the JAVA_HOME/jre/lib/ext directory (or as specified by System.getProperty(“java.ext.dirs”)).

1.3 AppClassLoader

The application classloader, which we work with the most, is implemented by sun.misc.Launcher$AppClassLoader. It loads files in the directory specified by System.getProperty(“java.class.path”), known as the classpath path.

2 parental delegation model

2.1 Principle of parental delegation model

Since JDK1.2, class loaders have introduced the parent delegate model, which looks like this:

The parent of two user-defined class loaders is AppClassLoader, and the parent of AppClassLoader is ExtClassLoader. ExtClassLoader does not have a parent class loader. In the code, The parent class loader of ExtClassLoader is null. BootstrapClassLoader also has no subclasses because it is implemented entirely by the JVM.

The parent-delegate model works like this: when a classloader receives a classload request, it first asks the parent classloader to load it, at every level, and when the parent classloader cannot find the class (based on the class’s fully qualified name), the subclass loader tries to load it itself.

To illustrate this, WE have implemented our own class loader called TestClassLoader, which uses the parent field to represent the parent class loader of the current loader, as defined below:

public abstract class ClassLoader {
...
    // The parent class loader fordelegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added *after* it. private final ClassLoader parent; . }Copy the code

Then use debug to look at the structure, as shown in the following figure

The first red box here is my own class loader, which corresponds to the bottom part of the figure above; The second box is the parent of the custom class loader, which you can see is AppClassLoader; The third box is the parent of the AppClassLoader, which is ExtClassLaoder; The fourth box is the parent of the ExtClassLoader and is null.

OK, here is an intuitive impression, later implementation principles will be introduced in detail.

2.2 Problems solved by this model

Why use the parent delegate model? What problems can it solve?

The parent delegate model was introduced after JDK1.2. According to the parent-delegate model principle, imagine that without the parent-delegate model, if a user wrote a fully qualified class named java.lang.Object and loaded it with his own classloader, At the same time, the BootstrapClassLoader loads the JAVa.lang.Object of the JDK itself in the rt.jar package, so that there are two copies of the Object class in memory. In this case, there are many problems, such as the specific class cannot be located based on the fully qualified name.

With the parent delegate model, all class loading operations are preferentially delegated to the parent class loader, so that even if a user defines a java.lang.Object, the user class loader will not load it again because the BootstrapClassLoader has detected that it has loaded the class.

Therefore, the parental delegation model ensures that classes are unique in memory.

2.3 Implementation principle of parental delegation model

Let’s look at the implementation of the parent delegate model from a source code perspective.

The JVM first calls the loadClassInternal method of the classLoader when loading a class. The source code for this method is shown below

// This method is invoked by the virtual machine to load a class. private Class<? > loadClassInternal(String name) throws ClassNotFoundException { // For backward compatibility, explicitly lock on'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             returnloadClass(name); }}else {
        returnloadClass(name); }}Copy the code

What this method does is call the loadClass method, which is implemented as follows

protected Class<? > loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, checkifThe class has already been loaded. > c = findLoadedClass(name);if(c == null) { long t0 = System.nanoTime(); Try {// If there is a parent class loader, delegate to the parent class loader firstif(parent ! = null) { c = parent.loadClass(name,false);
                } elseC = findBootstrapClassOrNull(name); c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrownifClass not found // from the non-null parent class loader} // If not found, call findClass and try to load the class yourselfif (c == null) {
                // If still not found, then invoke findClass inorder // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
            resolveClass(c);
        }
        returnc; }}Copy the code

Several key steps have been described in the source code.

Where the BootstrapClassLoader is called in the code is actually the native method of the call.

Thus, the core of the parent delegate model implementation is this loadClass method.

Implement your own class loader

3.1 Why implement your own classloader

To answer this question, start by thinking about what class loaders do (highlighted in bold).

3.1.1 The role of class loaders

What does a class loader do? Let’s go back to the source code above.

As we know, the JVM uses the loadClass method to find classes, so its first and most important function is to find class files in the specified path (the scan path for each class loader is given above).

Then, when the parent class loader says it has not loaded the target class, it will try to load the target class itself. This calls the findClass method.

protected Class<? > findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }Copy the code

He asked to return to a Class object instance can be found, here I through an implementation Class sun. Rmi. Rmic. Iiop. ClassPathLoader to explain what findClass.

Protected Class findClass(String var1) throws ClassNotFoundException {// Load the byte stream of the named Class from the specified path. Byte [] var2 = this.loadClassData(var1); // Create an instance of the class object through the ClassLoader defineClassreturn this.defineClass(var1, var2, 0, var2.length);
}
Copy the code

What he does is given in the comment, and as you can see, he eventually instantiates the class object through the defineClass method.

In addition, we can control the retrieval and processing of class file bytes. So, the second function: we can do some custom processing in byte stream parsing. For example, encryption and decryption.

Next, there seems to be a defineClass that lets us do something. The ClassLoader implementation looks like this:

protected final Class<? > defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {return defineClass(name, b, off, len, null);
}
Copy the code

It was lost by final, so it can’t be overwritten, so it seems that nothing can be done here.

To summarize:

  • Finds files in the specified path by loadClass.
  • The findClass method parses the class stream and instantiates the class object.
3.1.2 When do I need to implement my own class loader

When the class loader implementation provided by the JDK does not meet our needs, we need to implement our own class loader.

Existing application scenarios: OSGi, code hot deployment and other fields.

In addition, depending on what the class loader does above, there may be several scenarios where you need to implement your own class loader

  • When you need to find a class file in a custom directory (or network fetch)
  • Encryption and decryption of a class before it is loaded by the class loader (code encryption domain)

3.2 How to implement my own class loader

Next, implement a custom classloader that finds and loads the class in the custom class classpath.

package com.lordx.sprintbootdemo.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; /** * Custom ClassLoader * @author zhiminxu */ / Public Class TestClassLoader extends ClassLoader {private String classPath; public TestClassLoader(String classPath) { this.classPath = classPath; } // override the findClass method protected Class<? > findClass(String name) throws ClassNotFoundException {// getDate scans the class according to a custom path, Byte [] classData = getDate(name);if (classData == null) {
            throw new ClassNotFoundException();
        } else{// Generate an instance of classreturndefineClass(name, classData, 0, classData.length); }} private byte[] getDate(String name) {String path = classPath + file.separatorChar + name.replace('. ', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while((num = is.read(buffer)) ! = -1) { stream.write(buffer, 0 ,num); }return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnnull; }}Copy the code

Use custom class loaders

package com.lordx.sprintbootdemo.classloader; Public class MyClassLoader {public static void main(String[] args) throws ClassNotFoundException {// Customize the class classpath String classPath ="/Users/zhiminxu/developer/classloader"; // Custom class loader implementation: TestClassLoadertestClassLoader = new TestClassLoader(classPath); // Use a custom Class loader to load Class<? > object =testClassLoader.loadClass("ClassLoaderTest"); // The print here should be our custom class loader: TestClassLoader system.out.println (object.getClassLoader()); }}Copy the code

The experimental ClassLoaderTest Class is a simple Class that defines two fields. As shown in the figure below

The final print is as follows

PS: It is best not to put the experimental class (ClassLoaderTest) in the IDE’s project directory, because the IDE will load all the classes in the project into memory when run, so this class is not loaded by the custom classloader, but by the AppClassLoader.

3.3 The influence of class loaders on “equality” judgment

3.3.1 Impact on Object.equals(

The same custom classloader as above

Modify the MyClassLoader code

package com.lordx.sprintbootdemo.classloader; Public class MyClassLoader {public static void main(String[] args) throws ClassNotFoundException {// Customize the class classpath String classPath ="/Users/zhiminxu/developer/classloader"; // Custom class loader implementation: TestClassLoadertestClassLoader = new TestClassLoader(classPath); // Use a custom Class loader to load Class<? > object1 =testClassLoader.loadClass("ClassLoaderTest"); Class<? > object2 =testClassLoader.loadClass("ClassLoaderTest"); System.out.println(object1.equals(object2)); // Object1 and object2 are loaded using the same classloader. // Define a new class loader, TestClassLoadertestClassLoader2 = new TestClassLoader(classPath); Class<? > object3 =testClassLoader2.loadClass("ClassLoaderTest"); System.out.println(object1.equals(object3)); // Object1 and object3 are loaded with different class loaders. }}Copy the code

Print result:

true

false

The equals method compares memory addresses by default, so different classloader instances have their own memory space, even if the class has the same fully qualified name but has different classloaders.

So, the condition that equals is true for two classes in memory is that two class instances with the same fully qualified name are loaded with the same classloader instance.

3.3.2 Impact on Instanceof

Modify TestClassLoader, add main method to test, modify TestClassLoader as follows:

import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; /** * Custom ClassLoader * @author zhiminxu */ / Obtaining basic functions Public Class TestClassLoader extends ClassLoader {public static void main(String[] args) throws ClassNotFoundException { TestClassLoadertestClassLoader = new TestClassLoader("/Users/zhiminxu/developer/classloader");
        Object obj = testClassLoader.loadClass("ClassLoaderTest"); / / obj istestClassLoaderTest is loaded by AppClassLoader.falseSystem.out.println(obj instanceof ClassLoaderTest); } // Custom class scan path private String classPath; public TestClassLoader(String classPath) { this.classPath = classPath; } // override the findClass method protected Class<? > findClass(String name) throws ClassNotFoundException {// getDate scans the class according to a custom path, Byte [] classData = getDate(name);if (classData == null) {
            throw new ClassNotFoundException();
        } else{// Generate an instance of classreturndefineClass(name, classData, 0, classData.length); }} private byte[] getDate(String name) {String path = classPath + file.separatorChar + name.replace('. ', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while((num = is.read(buffer)) ! = -1) { stream.write(buffer, 0 ,num); }return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnnull; }}Copy the code

Print result:

false

Others affect the Class’s isAssignableFrom() and isInstance() methods for the same reason.

3.3.3 Additional notes

Don’t try to customize java.lang packages and try to load them with loaders. So it looks like this

Doing so will throw an exception directly

This exception is thrown during the validation process of calling defineClass and the source code is as follows

// Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if((name ! = null) && name.startsWith("java.")) {
    throw new SecurityException
        ("Prohibited package name: " +
         name.substring(0, name.lastIndexOf('. ')));
}
Copy the code

Several exceptions related to ClassLoader

If you want to know when the following exceptions are thrown, you just need to find out where they are thrown in the ClassLoader and then look at the logic.

4.1 a ClassNotFoundException

This anomaly, I believe we often encounter.

So, what exactly is causing this exception to be thrown?

If you look at the ClassLoader source, this exception is thrown when the JVM calls the loadClassInternal method.

Its statement is as follows:

// This method is invoked by the virtual machine to load a class. private Class<? > loadClassInternal(String name) throws ClassNotFoundException { // For backward compatibility, explicitly lock on'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             returnloadClass(name); }}else {
        returnloadClass(name); }}Copy the code

The loadClass method will throw this exception, so let’s look at the loadClass method

public Class<? > loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}
Copy the code

The overridden loadClass method is called

protected Class<? > loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, checkifThe class has already been loaded // findLoadedClass does not throw the exception class <? > c = findLoadedClass(name);if (c == null) {
            long t0 = System.nanoTime();
            try {
                if(parent ! C = parent. LoadClass (name, loadClass);false);
                } else{ c = findBootstrapClassOrNull(name); Throw thrown class // Throw thrown class // Throw thrown class // throw thrown class //if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass inorder // to find the class. long t1 = System.nanoTime(); C = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
            resolveClass(c);
        }
        returnc; }}Copy the code

Now look at the findClass method

/**
 * Finds the class with the specified <a href="#name">binary name</a>.
 * This method should be overridden by class loader implementations that
 * follow the delegation model for loading classes, and will be invoked by
 * the {@link #loadClass <tt>loadClass</tt>} method after checking the
 * parent class loader for the requested class.  The default implementation
 * throws a <tt>ClassNotFoundException</tt>.
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @returnThe resulting <tt>Class</tt> object * * @throws ClassNotFoundException * * @since 1.2 */ protected Class<? > findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }Copy the code

This exception is thrown when the class cannot be found.

This is why the above custom classloader overrides the findClass method and throws this exception if the class is not found.

At this point, it is clear why this exception is thrown: in all related classloaders in the parent delegate model, the target class is not present in each classloader’s scan path.

4.2 NoClassDefFoundError

This is also a common exception, and unfamiliar students can often be confused about when a ClassNotFoundException is thrown and when a NoClassDefFoundError is thrown.

If you want to do a ClassLoader search for this exception, you can see that it is possible to throw this exception in the defineClass method.

/ * * *... * * @throws ClassFormatError * If the data did not contain a valid class * * Because NoClassDefFoundError is under Error, Throws * @noClassDeffounderror * If <tt>name</tt> is not equal to the <a href="#name">binary
 *          name</a> of the class specified by <tt>b</tt>
 *
 * @throws  IndexOutOfBoundsException
 *          If either <tt>off</tt> or <tt>len</tt> is negative, or if
 *          <tt>off+len</tt> is greater than <tt>b.length</tt>.
 *
 * @throws  SecurityException
 *          If an attempt is made to add this class to a package that
 *          contains classes that were signed by a different set of
 *          certificates than this class, or if <tt>name</tt> begins with
 *          "<tt>java.</tt>". */ protected final Class<? > defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); Stringsource= defineClassSourceLocation(protectionDomain); Class<? > c = defineClass1(name, b, off, len, protectionDomain,source);
    postDefineClass(c, protectionDomain);
    return c;
}
Copy the code

PreDefineClass (); preDefineClass ();

/* Determine protection domain, and check that:
    - not define java.* class,
    - signer of this class matches signers for the rest of the classes inPackage. */ private ProtectionDomain preDefineClass(String name, ProtectionDomain pd)if(! checkName(name)) throw new NoClassDefFoundError("IllegalName: " + name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    if((name ! = null) && name.startsWith("java.")) {
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('. ')));
    }
    if (pd == null) {
        pd = defaultDomain;
    }

    if(name ! = null) checkCerts(name, pd.getCodeSource());return pd;
}
Copy the code

The source code for this check is as follows

// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) {
    if ((name == null) || (name.length() == 0))
        return true;
    if ((name.indexOf('/') != -1)
        || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
        return false;
    return true;
}
Copy the code

So, this exception is thrown because the class name verification failed.

However, this exception is not only thrown in the ClassLoader, but can be thrown in other places, such as frameworks, or Web containers, on a case-by-case basis.

5 concludes

This article first introduces the function of ClassLoader: mainly used to find a class from the specified path and load into memory, another is to determine whether two classes are equal.

The parent delegate model is mainly implemented in the loadClass method of ClassLoader in JDK.

Then code examples show how to implement your own class loader and how to use it.

Finally, two common classloader-related exceptions in our work are explained.