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.