Java-Hot Load Implementation

Keywords: Java source code

What is hot loading

Hot loading means that you can make the changes take effect without restarting the service.

Hot loading can significantly improve the efficiency of development and debugging. It is implemented by a Java-based class loader, but *** is not normally used in a formal production environment because of the insecurity of hot loading.

Hot Loaded VS Hot Deployment

Both hot loading and hot deployment, which can compile/deploy projects without restarting services, are implemented by Java-based class loaders.

Deployment Mode

  • Hot deployment redeploys the entire project at run time;
  • Hot load reloads the class at run time;

Implementation Principle

  • Hot deployment redeploys the entire application at runtime, which takes relatively high time.
  • Hot load reloads the class at runtime and starts a thread in the background to detect changes to the class.

Use scenarios

  • Hot deployments are typically used in production environments;
  • Hot loads are typically used in development environments and are difficult to monitor online because of security issues.

Get ready

Java Class Loading Mechanism

Java class life cycle: load->validation->prepare->parse->initialization->use->uninstall, the first five stages of class loading.

Of the five stages of class loading, only the loading stage can be customized by the user, while the validation, preparation, parsing, and initialization stages are all handled by the JVM.

Load

During the loading phase, the JVM does three things:

  1. Gets the binary byte stream that defines a class by its fully qualified name
  2. Convert the static storage structure represented by this byte stream into the runtime data structure of the method area
  3. Generates a java.lang.Class object in memory that represents this class as an access point to various data of this class in the method area.

There are three officially defined class loaders that determine the loading order through a parental delegation mechanism:

classloaderLoad Path
BootstrapClassLoaderAt the highest level in the class loader hierarchy, responsible for loading classes under the sun.boot.class.path path path, defaults to the jar package specified by the core API or -Xbootclasspath option in the jre/lib directory
ExtClassLoaderThe load path is java.ext.dirs, and the default is the jre/lib/ext directory or the jar package load in the directory specified by -Djava.ext.dirs
AppClassLoaderThe load path is java.class.path, which defaults to the value set in the environment variable CLASSPATH. You can also specify it through-classpath

By default, the keyword new or Class.forName is loaded through the AppClassLoader class loader

By default, if a class is to be loaded, it is given priority to its parent class for loading (until the top-level BootstrapClassLoader), and if neither parent class can be loaded, the class is not handed over to the child class for loading.

Verification

Ensure that byte codes are secure and do not compromise the security of virtual machines

Get ready

Determine the memory layout, determine memory traversal, and assign an initial value (note: there are also special cases)

analysis

Turn a symbol reference into a direct reference.

Initialization

Caller customized code. There are only five cases where initialization is mandatory:

  1. new (instantiated object), getstatic (get the value of a class variable, except when modified by final, its value is initialized when compiled in a constant pool), putstatic (assign a value to a class variable), and invokestatic (call to a static method);
  2. When calling a subclass, it is found that the parent class has not been initialized yet, and the parent class needs to be initialized immediately.
  3. The virtual machine starts, and the user executes the main class, which needs to be initialized immediately, such as the main method;
  4. Reflection calls to the class using the java.lang.reflect package method, which initializes;
  5. When using JDK 1.7 dynamic language support, if a java.lang.invoke.MethodHandle instance resolves to REF_getStatic, REF_putStatic, REF_ The invokeStatic's method handle, which corresponds to a class that has not been initialized, needs to be triggered first.

How to achieve hot loading

Custom Class Loader

A custom class loader is required.

Why do I need a custom class loader

Why do you need a custom class loader, and why can't you use AppClassLoader or ExtClassLoader for hot loading?

ClassLoader Source:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        //Verify that the class is loaded and that the JVM represents the same class as a class loader instance + class file
        //findLoadedClass returns unloaded if the class is loaded by a different class loader
        //So each subsequent rebuild of the class loader instance is required
        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;
    }
}

As you can see, Class<?> C = findLoadedClass (name) Once it finds that the class has been loaded, it will not reload, so it cannot be hot loaded.

How to customize the class loader

Inherit ClassLoader, default loadClass follows parental delegation mechanism, so override ClassLoader

public class CustomClassLoader extends ClassLoader{

    private static final String CLASS_FILE_SUFFIX = ".class";

    //Parent Loader for AppClassLoader
    private ClassLoader extClassLoader;

    public CustomClassLoader(){
        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.extClassLoader = j ;
    }

    protected Class<?> loadClass(String name, boolean resolve){

        Class cls = null;
        cls = findLoadedClass(name);
        if (cls != null){
            return cls;
        }
        //Get ExtClassLoader
        ClassLoader extClassLoader = getExtClassLoader();
        try {
            //Ensure that custom classes do not override core Java classes
            //Because ExtClassLoader cannot be loaded for classpath classes,
            //But if you switch to AppClassLoader here, it can load classes on the classpath.
            //Obviously loads first, so it's no turn to customize the loader and it becomes a different parent delegated loading mechanism
            cls = extClassLoader.loadClass(name);
            if (cls != null){
                return cls;
            }
        }catch (ClassNotFoundException e ){

        }
        cls = findClass(name);
        return cls;
    }

    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }

    private byte[] loadClassData(String className) {
        // Read Class file?
        InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+CLASS_FILE_SUFFIX);
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        // Write byteStream
        int len =0;
        try {
            while((len=is.read())!=-1){
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // Convert to Array
        return byteSt.toByteArray();
    }

    public ClassLoader getExtClassLoader(){
        return extClassLoader;
    }
}

Why get the ExtClassLoader class loader first? In fact, the design inside Tomcat is used for reference to avoid some core classes being overwritten by custom class loaders. For example, java.lang.Object.

Why get ExtClassLoader class loader instead of AppClassLoader? That's because if you get AppClassLoader to load, the AppClassLoader loader can load all classes on the classpath, including test classes naturally, so it's no longer your turn to customize the loader, so it's not a parent-delegated rule.

Timing monitoring class modifications

Use ScheduledThreadPoolExecutor to periodically monitor whether files are modified.

Record the last modification time of the file when the program starts. Then periodically check to see if the last modification time of the file has changed. If it is changed, regenerate the class loader to replace it.

Test class:

public class Test {

    public void test() {
        System.out.println("Test.test(). Version 1");
    }
}

Timed Task Class:

public class WatchDog implements Runnable{

    private Map<String,FileDefine> fileDefineMap;

    public WatchDog(Map<String,FileDefine> fileDefineMap){
        this.fileDefineMap = fileDefineMap;
    }

    @Override
    public void run() {
        File file = new File(FileDefine.WATCH_PACKAGE);
        File[] files = file.listFiles();
        for (File watchFile : files){
            long newTime = watchFile.lastModified();
            FileDefine fileDefine = fileDefineMap.get(watchFile.getName());
            long oldTime = fileDefine.getLastDefine();
            //If the file is modified, rebuild the accumulator to load the new file
            if (newTime!=oldTime){
                fileDefine.setLastDefine(newTime);
                loadMyClass();
            }
        }
    }

    public void loadMyClass(){
        try {
            //Rebuild class loader, reload class
            CustomClassLoader customClassLoader = new CustomClassLoader();
            Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
            //Test test = (Test) cls.newInstance() cannot be used
            //Because the class loaders that load the two classes are different
            Object test = cls.newInstance();
            Method method = cls.getMethod("test");
            method.invoke(test);
        }catch (Exception e){
            System.out.println(e);
        }
    }
}

optimization

Determining whether two classes are equal in Java will see if their class loaders are the same, in addition to whether they have the same class files.

So even if the same class file is loaded by two different class loaders, their types are different.

The WatchDog class is loaded by new, so it is loaded by AppClassLoader by default, and the declared type of the test variable is a property of the WatchDog method, so it is also loaded by AppClassLoader, so the two classes are different, so you cannot use Test = (Test) cls.newInstance() directly, which will report a ClassCastException class type conversion exception.

Solution

Through the interface.

By default, if an interface is implemented, it is generally a subclass loader-based interface.

That is, if there are no special requirements, such as A implements B if A's loader is custom. Then the loader for the B interface is the same as the subclass.

So you can make the AppClassLoader class loader load interfaces by modifying the custom loader.

if ("com.example.watchfile.ITest".equals(name)){
    try {
        cls = getSystemClassLoader().loadClass(name);
    } catch (ClassNotFoundException e) {

    }
    return cls;
}

Create an interface:

public interface ITest {

    void test();
}

The class loaders on both sides are the same:

CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
ITest test = (ITest) cls.newInstance();
test.test();

Reference resources:

Fucking great! Write your own handwriting on a hot load~

Originally hot loading was so simple, write a Java hot loading manually

Posted by jmcc on Tue, 23 Nov 2021 10:41:54 -0800