The JVM custom class loader loads all classes and jar s under the classPath specified

Keywords: Programming Java jvm Tomcat

1, Classloader type in JVM

From the perspective of Java virtual machine, there are only two different classloaders: boot classloader and other classloaders. 1. Bootstrap classloader: This is implemented by c + +, which is mainly responsible for the core api under the JAVA_HOME/lib directory or the jar packaging specified by the - Xbootclasspath option. 2. Other Class loaders: implemented by java, whose Class objects can be found in the method area. Here it is subdivided into several loaders a). Extension ClassLoader: it is used to load all class libraries (jars) in the JAVA_HOME/lib/ext directory, or in the path specified by the - Djava.ext.dirs system variable. Developers can directly use the Extension ClassLoader. The path specified by the java.ext.dirs system variable can be viewed through System.getProperty("Java. Ext.dirs"). b). Application ClassLoader: responsible for the packaging of classes and jar s in the directory indicated by Java - class path or - Djava.class.path. Developers can use this classloader directly. This is the default loader for a program when no custom classloader is specified. c). User ClassLoader: during program operation, class files are loaded dynamically through subclasses of java.lang.ClassLoader to reflect the dynamic real-time class loading characteristics of Java.

2, Why to customize class loader

Distinguish classes with the same name: suppose there are many independent applications deployed on the tomcat application server, and they have many classes with the same name but different versions. To distinguish different versions of classes, of course, each application needs to have its own independent class loader. Otherwise, it is impossible to distinguish which one to use. Class library sharing: each web application can use its own version of jar in tomcat. However, for example, Servlet-api.jar, Java Native packages and custom added Java class libraries can be shared with each other. Enhanced class: the class loader can rewrite and overwrite the class during the loadClass, during which the class can be enhanced in functionality. For example, use javassist to add and modify the class function, or add the dynamic agent used in aspect oriented programming, and debug and other principles. Hot replacement: upgrade the software while the application is running, without restarting the application. For example, JSP update and replacement in toccat server.

3, Custom class loader

3.1 ClassLoader implementation of user-defined classloader To implement a custom class loader, you need to inherit ClassLoader, which is an abstract class that is responsible for loading the objects of classes. There are at least three ways to customize ClassLoader:

loadClass,findClass,defineClass.    

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

loadClass: when the JVM loads a class, it uses the loadClass() method of ClassLoader to load the class. The loadClass uses the parental delegation mode. If you want to change the parent delegation mode, you can modify the loadClass to change the loading mode of the class. The parental delegation model is not covered here. findClass: ClassLoader loads classes through the findClass() method. The custom class loader implements this method to load the required classes, such as files and byte streams under the specified path. definedClass: definedClass is used in findClass. By calling the byte array of a class file, you can generate a class object in the method area, that is, findClass implements the function of class loading.

Paste a section of ClassLoader loadClass source code, see the real face

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) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // 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);
        }
        return c;
    }
}

Source code description

/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/

The default implementation of this method looks up the class in the following order: call findLoadedClass(String) method to check whether the class has been loaded Use the parent loader to call the loadClass(String) method. If the parent loader is Null, the class loader loads the built-in loader of the virtual machine and calls the findClass(String) method to load the class. If the corresponding class is found successfully according to the above steps, and the value of the resolve parameter received by the method is true, then the resolveClass(Class) method is called to process the class. The subclass of ClassLoader is better to override findClass(String) instead of this method. Unless overridden, this method is synchronized by default throughout the load process (thread safe).

resolveClass: Class loading must be linked. Link refers to adding a single Class to the Class tree with inheritance relationship. This method is used by Classloader to link a Class. If the Class has already been linked, then this method only makes a simple return. Otherwise, the Class will be Gamma The Execution description in the specification is linked.

3.2 implementation of custom class loader

According to 3.1, after inheriting ClassLoader, the findClass method is overridden to load the class on the specified path. First paste the custom class loader.

package com.chenerzhu.learning.classloader;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author chenerzhu
 * @create 2018-10-04 10:47
 **/
public class MyClassLoader extends ClassLoader {
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String name) {
        try {
            return Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

The above is the user-defined class loader. The function is to load the class of the specified path. Let's see how to use it.  

package com.chenerzhu.learning.classloader;

import org.junit.Test;

/**
 * Created by chenerzhu on 2018/10/4.
 */
public class MyClassLoaderTest {
    @Test
    public void testClassLoader() throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
        Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
        Object obj = clazz.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

First, create the myclassloader object myclassloader through the construction method, and specify the Hello.class to load the path of src/test/resources/bean/Hello.class (of course, here is just an example, directly specifying the path of a class). Then load the class object of Hello through the myclassloader method loadClass, and instantiate the object. The following is the output. It can be seen that the instantiation is successful, and the classloader uses myclassloader.

com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca

4, Class class unload

The class and Meta information in the JVM is stored in the PermGen space area (after JDK 1.8, it is stored in the MateSpace). If there are many class files loaded, it may cause metadata space overflow. Causes a java.lang.OutOfMemory exception. For some classes, we may only need to use them once, and they are no longer needed, or we may modify the class file, and we need to reload the new class, so oldclass is no longer needed. So you need to unload the class in the JVM. The class in the JVM can only be recycled by GC if the following three conditions are met, that is, the class is unloaded:

  1. All instances of this class have been GC.
  2. The java.lang.Class object of this class is not referenced anywhere.
  3. The ClassLoader instance that loaded the class has been GC.

It's easy to understand that the ClassLoader instance of the Class to be unloaded can be unloaded if it has been GC and there is no relevant reference. In other words, the JVM clears the binary data of the Class in the method area. In the life cycle of virtual machine, the classes loaded by the classloaders of JVM will always reference these classloaders, and these classloaders will always reference the Class objects of the classes they load. Therefore, these Class objects are always accessible and will not be unloaded. The user-defined Class loader can load classes that can be unloaded. Although Class can be uninstalled if the above three conditions are met, we can't control the timing of GC, so we can't control the uninstallation of Class.

5, JVM custom class loader loads all classes and jar s under the specified classPath

After the above points, you can now implement the JVM custom class loader to load all classes and jars under the specified classPath. There is no limit to the location of class and jar. As long as it is under the classPath path path, it will be loaded into the JVM. However, some web application servers are limited to load. For example, tomcat loads classPath + "/ classes" for each application and classPath + "/ lib" for jar. Here is the code

  

package com.chenerzhu.learning.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author chenerzhu
 * @create 2018-10-04 12:24
 **/
public class ClassPathClassLoader extends ClassLoader{

    private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
    private String classPath;

    public ClassPathClassLoader() {
    }

    public ClassPathClassLoader(String classPath) {
        if (classPath.endsWith(File.separator)) {
            this.classPath = classPath;
        } else {
            this.classPath = classPath + File.separator;
        }
        preReadClassFile();
        preReadJarFile();
    }

    public static boolean addClass(String className, byte[] byteCode) {
        if (!classMap.containsKey(className)) {
            classMap.put(className, byteCode);
            return true;
        }
        return false;
    }

    /**
     * Here only the class in the classMap of myclassLoader is uninstalled, and the class in the virtual machine is uninstalled
     * Class The uninstall of is uncontrollable
     * The unloading of custom class requires that MyClassLoader does not have conditions such as reference
     * @param className
     * @return
     */
    public static boolean unloadClass(String className) {
        if (classMap.containsKey(className)) {
            classMap.remove(className);
            return true;
        }
        return false;
    }

    /**
     * Follow the parental delegation rules
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        if (classMap.containsKey(className)) {
            return classMap.get(className);
        } else {
            return null;
        }
    }

    private void preReadClassFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanClassFile(file);
            }
        }
    }

    private void scanClassFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".class")) {
                try {
                    byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
                    String className = file.getAbsolutePath().replace(classPath, "")
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    addClass(className, byteCode);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanClassFile(f);
                }
            }
        }
    }

    private void preReadJarFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                //String className = name.replace(File.separator, ".").replace(".class", "");
                String className = name.replace("\\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }


    public void addJar(String jarPath) throws IOException {
        File file = new File(jarPath);
        if (file.exists()) {
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

The code for how to use is not pasted. It is the same as the custom class loader in Section 3.2. Only the parameters of the construction method become classPath. There is code at the end of the article. When creating the MyClassLoader object, all classes under the specified classPath and the classes in the jar will be automatically added to the classMap. The classMap maintains the relationship between the className and the classCode bytecode, which is just a buffer function to avoid reading from the file every time. Each time a custom class loader loads a class, it will first find out in the JVM whether the className class has been loaded. If it doesn't exist, it will get it from the classMap. If it can't get it, it's a load error. I have a technology exchange group of my own. Students who are interested in Java technology and architecture technology are welcome to work for 1-5 years. Developers are welcome to study together. There are also interview questions and architecture materials in mutual discussion (private letter reply Java)

Six, last

At this point, the JVM custom class loader has finished loading all classes and jar s under the specified classPath. This blog took two days to write, in the process of writing consciously to understand the details of a lot of code, harvest a lot. Originally, I just wanted to add support dynamic class to the Quartz console page task recently, but I ran to the classloader pit unconsciously, and I will take this opportunity to summarize. Of course, the above content does not guarantee the correctness, so I hope you can point out the mistakes and help me to correct the existing cognition and make progress together

Posted by andyd34 on Sun, 08 Mar 2020 00:58:32 -0800