preface

For Java applications, hot loading means that Class files are reloaded in the JVM during runtime without restarting the application. For some projects, some companies upgrade more frequently in the form of incremental upgrades for the stability of online applications. If you simply replace the Class file, the application is still using the old code. At this point, hot loading becomes very important. This article is a hot load implementation of the way to introduce, I hope to help you.

The core of this functionality is to dynamically replace Class objects that already exist in the JVM. So we must first understand the JVM’s classloading mechanism.

JVM class loading mechanism

Class loading process

First, the flow of Java code into the JVM is as follows, as I have simply drawn:

Once loaded into the virtual machine, we can normally use the keyword new object. You can see that the key to loading here is the Classloader. There are three types of loaders available in Java.

  • BootstrapClassloader
  • ExtClassloader
  • AppClassloader

The BootstrapClassloader is called the boot class loader and is used to load the JRE core class library. It is implemented in C++. All libraries under %JAVA_HOME%/lib.

ExtClassloader extends class loader to load all class libraries in %JAVA_HOME%/lib/ext.

System Classloader loads all class libraries in the %CLASSPATH% path.

Parents delegate

Now that we’re talking about these three loaders, we have to mention the famous parent delegate mechanism. Before introducing this mechanism, let’s look at what it solves. The virtual machine uses this mechanism to ensure that a class is not loaded repeatedly by multiple classloaders and that the core API is not tampered with. OK, a picture is worth a thousand words.

In general, if we do not use a custom class loader, the program defaults to the application class loader, which is the system class loader. When a classloader needs to load a.class file, it first delegates the task to its parent classloader, recursively, and only loads the class if the parent classloader does not load it.

I think it should be clear that if a Class file is loaded by different Class loaders, the virtual machine will assume that they are not the same Class. A ClassCastException is reported if the conversion is performed.

Even if you create a class with exactly the same name as the fully qualified class in the core library (such as java.lang.String), the virtual machine will treat it as two classes (they live at different addresses in the virtual machine) and the system will be safe to execute.

We can look at the source code to learn how to implement parental delegation, from the ClassLoader class.

protectedClass<? > loadClass(String name,boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First check if it has been loaded by the current class loaderClass<? > c = findLoadedClass(name);if (c == null) {
            long t0 = System.nanoTime();
            try {
            	// Recurse this method if there is a parent class loader
                if(parent ! =null) {
                    c = parent.loadClass(name, false);
                } else {
                $ExtClassloader; $ExtClassloader; $ExtClassloaderc = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
            }
            if (c == null) {
                // Throw a ClassNotFoundException
                // If the BootstrapClassloader is not loaded, call the findClass method to load it
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
        // link to add a single Class to an inherited Class tree
            resolveClass(c);
        }
        returnc; }}Copy the code

Both ExtClassLoader and ExtClassLoader inherit from URLClassLoader, so when you call the findClass method, you call the parent URLClassLoader.

By looking at the source code, you can find:

  • ExtClassLoaderThe loading path isSystem.getProperty("java.ext.dirs")
  • AppClassloaderThe loading path isSystem.getProperty("java.class.path")
  • BootstrapClassloaderThe loading path isSystem.getProperty("sun.boot.class.path")

This also indirectly supports the conclusion of the class loader loading path introduced earlier.

How to delegate the Launcher$ExtClassloader to AppClassloader when findClass fails to find the corresponding Class?

If findClass was not found in URLClassLoader, throw ClassNotFoundException:

protectedClass<? > findClass(final String name)
    throws ClassNotFoundException
{
    finalClass<? > result;try {
        result = AccessController.doPrivileged(
            newPrivilegedExceptionAction<Class<? > > () {publicClass<? > run()throws ClassNotFoundException {
                    String path = name.replace('. '.'/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if(res ! =null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw newClassNotFoundException(name, e); }}else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name); // An exception is thrown
    }
    return result;
}
Copy the code

The loadClass method is recursive, so the exception is thrown to the AppClassloader instance and eventually caught by ClassNotFoundException. AppClassloader completes the findClass operation.

In addition, we looked at the virtual machine entry sun.misc.Launcher class (some of the code has been simplified for ease of view) :

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher(a) {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher(a) {
        // Create the extension class loader
        ClassLoader extcl;
        try {
        	// Create an ExtClassLoader whose parent is null
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
        	// Set the parent loader of AppClassLoader to ExtClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // Set AppClassLoader to the thread context classloader
        Thread.currentThread().setContextClassLoader(loader);
    }
    /* * Returns the class loader used to launch the main application. */
    public ClassLoader getClassLoader(a) {
        return loader;
    }
    /* * The class loader used for loading installed extensions. */
    static class ExtClassLoader extends URLClassLoader {}
        /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */
    static class AppClassLoader extends URLClassLoader {}}Copy the code

The parent of the AppClassLoader is ExtClassLoader, and the parent of ExtClassLoader is null.

We can print it:

public class PrintClassloader {

    public static void main(String[] args) {
        System.out.println(PrintClassloader.class.getClassLoader());
        System.out.println(PrintClassloader.class.getClassLoader().getParent());
        System.out.println(PrintClassloader.class.getClassLoader().getParent().getParent());
        System.out.println(Thread.currentThread().getContextClassLoader());
      / / DriverManager. GetConnection (" JDBC: mysql: / / 127.0.0.1:3306 / mysqlDB ", "root", "root");}}Copy the code

The output result is:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@61bbe9ba
null
sun.misc.Launcher$AppClassLoader@18b4aac2
Copy the code

As we expected.

All assigned

Global delegation means that when a ClassLoader loads a class, unless another ClassLoader is explicitly used, the classes that that class depends on and references are also loaded by that ClassLoader.

Don’t you understand? Simply put, whatever class loader is used for the entry to the program, then the objects from the following class new also use that class loader.

Look at a practical example:

Let’s take a look at the code to obtain the Mysql connection:

DriverManager.getConnection("JDBC: mysql: / / 127.0.0.1:3306 / mysqlDB"."root"."root");
Copy the code

First, DriverManager is provided by the rt.jar package, which is loaded by the BootstrapClassloader. The database vendor provides the implementation of the driver using SPI technology. The BootstrapClassloader will not be able to load the package after the reference is in the classpath. Let’s take a look at how to break this mechanism to complete the load and see how to get the connection (omit some of the code) :

public static Connection getConnection(String url, String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if(user ! =null) {
        info.put("user", user);
    }
    if(password ! =null) {
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));
}
Copy the code

The Reflection. GetCallerClass () method is to obtain the caller’s class loader, here is our caller PrintClassloader, as an ordinary class, its natural is AppClassloader class loader. Continue with the following code:

 private static Connection getConnection( String url, java.util.Properties info, Class
        caller) throws SQLException {

        // Get the caller's class loader.ClassLoader callerCL = caller ! =null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            if (callerCL == null) {
            	// If not, get the class loader in the thread context, otherwise apply the class loader
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        SQLException reason = null;
        for(DriverInfo aDriver : registeredDrivers) {
        // Important function isDriverAllowed
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println(" trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if(con ! =null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return(con); }}catch (SQLException ex) {
                    if (reason == null) { reason = ex; }}}}throw new SQLException("No suitable driver found for "+ url, "08001");
    }
Copy the code

Whether to allow loading:

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver ! =null) { Class<? > aClass =null;
        try {
        	// Use the specified class loader to load
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
         result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}
Copy the code

You can see that the final reflection is loaded in the form of the specified class loader.

Thermal loading

Finally, we arrived at the exciting hot loading link. As a result of the above, we know that we just need to provide our own class loader and override the loadClass method to complete our own class loading mechanism. Of course, we also need a folder listener to reload the.class file in the directory if it changes.

Dynamic class loaders

First, we need to control the changes to the.class files that we need to listen for in the directory, and we need to reload only when those changes occur. Other system native classes such as java.lang.String are loaded by the original system class loader (following the parent delegate mechanism).

All the code is presented directly here.

public class DynamicClassLoader extends URLClassLoader {


    private finalMap<String, Class<? >> classCache =new ConcurrentHashMap<>();

    // All classes that need to be loaded ourselves
    private final Map<String /** classname **/, File> fileCache = new ConcurrentHashMap<>();


    public DynamicClassLoader(a) {
        super(new URL[0]);
    }

    @Override
    publicClass<? > loadClass(String name)throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // Find if there are already class objects in the local classLoader namespace
            //  final Class<?> c = findLoadedClass(name);
            finalClass<? > c = classCache.get(name);if (c == null) {
                if (fileCache.containsKey(name)) {
                    throw new ClassNotFoundException(name);
                } else {
                    // This class does not need to be loaded (such as java.lang.String or a path we did not specify) to the AppClassloader
                    returngetSystemClassLoader().loadClass(name); }}returnc; }}/** * Recursively adds files in the specified directory to the class loader's load path */
    public void addURLs(String directory) throws IOException {
        Collection<File> files = FileUtils.listFiles(
                new File(directory),
                new String[]{"class"},
                true);
        for(File file : files) { String className = file2ClassName(file); fileCache.putIfAbsent(className, file); }}public void load(a) throws IOException {
        for (Entry<String, File> fileEntry : fileCache.entrySet()) {
            final File file = fileEntry.getValue();
            this.load(file); }}privateClass<? > load(File file)throws IOException {
        final byte[] clsByte = file2Bytes(file);
        finalString className = file2ClassName(file); Class<? > defineClass = defineClass(className, clsByte,0, clsByte.length);
        classCache.put(className, defineClass);
        return defineClass;
    }


    private byte[] file2Bytes(File file) {
        try {
            return IOUtils.toByteArray(file.toURI());
        } catch (IOException e) {
            e.printStackTrace(System.err);
            return new byte[0]; }}private String file2ClassName(File file) throws IOException {
        final String path = file.getCanonicalPath();
        final String className = path.substring(path.lastIndexOf("/classes") + 9);
        return className.replaceAll("/".".").replaceAll(".class".""); }}Copy the code

AddURLs is the add monitor directory, which recursively returns all.class files in that directory. You can load the vm using the load method.

The most crucial of these is the defineClass method, which loads the.class binary into the virtual machine. Of course, the class can only be loaded once. If called again, an error is reported and the class has been loaded.

The classCache is used to hold classes that have already been loaded by our custom loader, and it can determine if the current Class needs to be loaded. If not, hand it over to the system class loader.

How do you use it?

// Initializes the classloader
DynamicClassLoader classLoader = new DynamicClassLoader();
classLoader.addURLs("/Users/pleuvoir/dev/space/git/hot-deploy/target/classes/io/github/pleuvoir");
classLoader.load();
Copy the code

This completes the load.

Program entrance

public class Bootstrap {


    public static void main(String[] args) throws Exception {
        // Initializes the classloader
        DynamicClassLoader classLoader = new DynamicClassLoader();
        classLoader.addURLs(Const.HOT_DEPLOY_DIRECTORY);
        classLoader.load();
        start0(classLoader);
    }

    public void start(a) {
        final Mock mock = new Mock();
        mock.say();
    }

    public static void start0(ClassLoader classLoader) throws Exception {
        // Start the class file listener
        ClassFileMonitor.start();
        // Use the global delegateClass<? > startupClass = classLoader.loadClass("io.github.pleuvoir.Bootstrap");
        Object startupInstance = startupClass.getConstructor().newInstance();
        String methodName = "start"; Method method = startupClass.getMethod(methodName); method.invoke(startupInstance); }}Copy the code

Let’s look at the program entry class. Here we use our custom loader to call the load method to load the class file in the specified directory. At the same time, the class file listener is started in start0 (more on that below).

The reason for using reflection to call the class’s methods here is to use the global delegate mechanism. Since Bootrap is loaded by our custom classloader, the New Mock() object uses our custom classloader after it calls the start method. Imagine using the new Bootstrap().start() method. What class loader is used? The answer is AppClassLoader. In this case, the new object we create is not our custom class loader.

File listener

To implement the file listener, we use apache common-io package to complete the listening file changes, in fact, their own implementation is also very simple. To see if the lastModify property of the file has changed.

public class ClassFileMonitor {


    public static void start(a) throws Exception {
        IOFileFilter filter = FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter(".class"));
        FileAlterationObserver observer = new FileAlterationObserver(new File(Const.HOT_DEPLOY_DIRECTORY), filter);
        observer.addListener(new ClassFileAlterationListener());
        FileAlterationMonitor monitor = new FileAlterationMonitor(Const.HOT_DEPLOY_CLASS_MONITOR_INTERVAL, observer);
        monitor.start();
        System.out.println("Hot load file listening started.");
    }

    private static class ClassFileAlterationListener extends FileAlterationListenerAdaptor {

        @Override
        public void onFileChange(File file) {
            System.out.println("The file has changed." + file.getAbsolutePath());
            try {
                // Initializes the classloader
                DynamicClassLoader classLoader = new DynamicClassLoader();
                classLoader.addURLs(Const.HOT_DEPLOY_DIRECTORY);
                classLoader.load();

                Bootstrap.start0(classLoader);

            } catch (Throwable e) {
                e.printStackTrace(System.err);
            } finally{ System.gc(); }}}}Copy the code

It makes sense to recreate the classloader when the.class file in the listening directory changes. And call the function entry method we provided. The reason why the classloader is recreated is because the classes loaded by the original classloader cannot be unloaded, so you need to create a new classloader from scratch.

The problem here is that if the previous business method start contained code that could not exit, such as an infinite loop, it would continue to execute. In addition, system.gc () cannot properly reclaim the classloaders created previously, causing classloaders to leak.

After the language

This paper is a practice of dynamic hot replacement, I hope to help you. Since this article is only a demo, it does not consider all aspects. Interested readers can try it out for themselves, such as dynamically loading jars and doing hot replacement with Spring. The code has been uploaded github.com/pleuvoir/ho… For your reference.