Talk about class loading process, class loading mechanism and custom class loader in detail

Keywords: Java JDK jvm network

I. Introduction

When a program uses a class, if the class has not been loaded into memory, the JVM will load the class through three steps: loading, linking, and initializing.

2, Class loading, linking, initialization

1, loading

Class loading refers to reading class files into memory and creating a java.lang.Class object for them. Class loading is done by class loader, which is provided by JVM. Our developers can also implement their own ClassLoader by inheriting ClassLoader.

1.1 class source loaded

  • Load class file from local file system

  • Load class file from JAR package

  • Load class files over the network

  • Compile a java source file dynamically and load it.

2. Link to class

By loading the Class, a Class object has been created in memory. Links are responsible for merging binary data into the JRE. The link needs to pass three stages: verification, preparation and resolution.

2.1, validation

The validation phase is used to check whether the loaded classes have the correct internal structure and coordinate with other classes. That is, whether to meet the constraints of java virtual machine.

2.2. Preparation

The class preparation stage is responsible for allocating memory for class variables of the class and setting the default initial value.

2.3, analysis

We know that references actually correspond to memory addresses. Think about such a question. When writing code, using references and methods, does the class know the memory address of these reference methods? Obviously, I don't know, because the class hasn't been loaded into the virtual machine, you can't get these addresses.

For example, for a method call, the compiler will generate a symbolic reference containing the class, target method name, receive parameter type and return value type of the target method to refer to the method to be called.

The purpose of the parsing phase is to parse these symbolic references into actual references. If a symbol reference points to an unloaded class, or a field or method of an unloaded class, parsing triggers the loading of that class (but not necessarily parsing and initialization).

3. Class initialization

In the initialization stage of class, virtual machine mainly initializes class variables. The virtual machine calls the < clinit > method to initialize the class variables.

There are two ways to initialize class variables in java classes:

  1. Initialize at definition time

  2. Initialize in static initialization block

3.1. < clinit > method related

The virtual opportunity collection class and the class variables and class methods in the parent class are combined into < clinit > methods, which are initialized according to the defined order. The virtual opportunity ensures that the parent class's < clinit > method is executed before the child class's < clinit > method is executed.

 

Therefore, the first < clinit > method executed in the virtual machine must be the java.lang.Object method.

 

public class Test {
    static int A = 10;
    static {
        A = 20;
    }
}

class Test1 extends Test {
    private static int B = A;
    public static void main(String[] args) {
        System.out.println(Test1.B);
    }
}
//Output result
//20

It can be seen from the output that the static initialization block of the parent class is initialized before the static variables of the child class are initialized, so the output result is 20, not 10.

If there are no static variables and methods in the class or parent class, the virtual machine will not generate < clinit > methods for them.

Interface is different from class in that the clinit method of the parent interface does not need to be executed first. The parent interface is initialized only if the variables defined in the parent interface are used. In addition, the implementation class of the interface will not execute the clinit method of the interface during initialization.

public interface InterfaceInitTest {
    long A = CurrentTime.getTime();

}

interface InterfaceInitTest1 extends InterfaceInitTest {
    int B = 100;
}

class InterfaceInitTestImpl implements InterfaceInitTest1 {
    public static void main(String[] args) {
        System.out.println(InterfaceInitTestImpl.B);
        System.out.println("---------------------------");
        System.out.println("Current time:"+InterfaceInitTestImpl.A);
    }
}

class CurrentTime {
    static long getTime() {
        System.out.println("Loaded InterfaceInitTest Interface");
        return System.currentTimeMillis();
    }
}
//Output result
//100
//---------------------------
//InterfaceInitTest interface loaded
//Current time: 156015880660

It is verified from the output that for an interface, only class variables that actually use the parent interface will actually load the parent interface. This is not the same as normal class loading.

Virtual opportunity ensures that the < clinit > method of a class is properly locked and synchronized in a multithreaded environment. If multiple threads initialize a class at the same time, only one thread will execute the class's < clinit > method. Other threads need to block and wait until the active thread finishes executing the < clinit > method.

 

public class MultiThreadInitTest {
    static int A = 10;
    static {
           System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread() + "start");
            System.out.println(MultiThreadInitTest.A);
            System.out.println(Thread.currentThread() + "run over");
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
//Output result
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over

From the output, we can see that: only the first thread has initialized MultiThreadInitTest once, and the second thread has been blocked waiting for the first thread to finish initialization.

3.2 class initialization time

  1. When the virtual machine is started, the user specified main class is initialized;

  2. When a new instruction is used to create a new target class instance, the target class of the new instruction is initialized;

  3. When a static method is called or a static variable is used, initialize the static variable or the class of the method;

  4. The initialization of subclass will trigger the initialization of parent class;

  5. If an interface defines a default method, the initialization of the interface's class will be triggered if it is implemented directly or indirectly;

  6. When using the reflection API to make a reflection call on a class, initialize the class;

  7. Class.forName() triggers class initialization

3.3 initialization of final definition

Note: for a constant defined with final, if the value has been determined at compile time, initialization will not be triggered at reference time, because it has been determined at compile time, which is "macro variable". If it cannot be determined at compile time, initial use will result in initialization.

public class StaticInnerSingleton {
    /**
     * Use static inner class to implement a single example:
     * 1: Thread safety
     * 2: Lazy loading
     * 3: Non deserialization security, that is, the object obtained by deserialization is not the same as the singleton object during serialization, which violates the singleton principle
     */
    private static class LazyHolder {
        private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return LazyHolder.INNER_SINGLETON;
    }
}

Look at this example, the singleton pattern static inner class implementation. We can see that the single instance uses the final definition, but it cannot be determined at compile time, so the static inner class load will be triggered when the StaticInnerSingleton.getInstance() method is used for the first time, that is, delayed load.

Here I would like to point out that if the variables defined by final cannot be determined at compile time, class initialization will still occur at use time.

3.4. ClassLoader will only load classes, not initialize them

public class Tester {
    static {
        System.out.println("Tester Static initialization block of class");
    }
}

class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //The following statement simply loads the Tester class
        classLoader.loadClass("loader.Tester");
        System.out.println("System loading Tester class");
        //The following statement initializes the Tester class
        Class.forName("loader.Tester");
    }
}
//Output result
//System load Tester class
//Static initialization block of Tester class

It is proved from the output that ClassLoader will only load the class and will not initialize it; using Class.forName() will force the initialization of the class.

3, Classloader

The class loader is responsible for loading the. Class file (whether jar, local disk, network acquisition, etc.) into memory and generating the corresponding java.lang.Class object for it. When a class is loaded into the JVM, it will not be loaded for the second time.

How to judge the same class?

Each class uses the fully qualified class name (package name + class name) and class loader as a unique ID in the JVM, so if the same class uses different class loaders, it can be loaded into the virtual machine, but they are not compatible with each other.

1. JVM classloader classification

1.1,Bootstrap ClassLoader

Bootstrap ClassLoader is the root class loader, which is responsible for loading java's core class library. The root loader is not a subclass of ClassLoader. It is implemented in C + +.

public class BootstrapTest {
    public static void main(String[] args) {
        //Get all URL arrays loaded by the root loader
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        Arrays.stream(urLs).forEach(System.out::println);
    }
}
//Output result
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes

The root loader is responsible for loading the jar package under% JAVA_HOME%/jre/lib (as well as the class specified by the virtual machine parameter - Xbootclasspath).

When we unzip rt.jar, we can see that the class library we often use is in this jar package.

1.2 ,Extension ClassLoader

Extension ClassLoader is an extension class loader, which is responsible for loading the jar package of% JAVA_HOME%/jre/ext or java.ext.dirs system familiar with the specified directory. You can put your own toolkit in this directory, which is convenient for you to use.

1.3, System ClassLoader

System ClassLoader is the system (application) classloader, which is responsible for loading the JAR package and class path specified by the - CLASSPATH option from Java command, java.class.path system attribute, or CLASSPATH environment variable. The program can get the System ClassLoader through ClassLoader.getSystemClassLoader(). If it is not specified, the user-defined classloader takes the System ClassLoader as the parent loader by default.

4, Class loading mechanism

1.1. The main class loading mechanism of JVM.

  1. Overall responsibility: when a Class loader is responsible for loading a Class, other classes that the Class depends on and references are also loaded by the Class loader, unless it is shown that another Class loader is used for loading.

  2. Parent Class delegation (parent delegation): first ask the parent loader to try to load the Class. Only when the parent loader fails to load will the Class loader try to load the Class from its own Class path.

  3. Caching mechanism: the caching mechanism will cache the loaded class. When a class needs to be used in the program, the class loader first searches for the class from the cache area. Only when the class does not exist in the cache, the system will read the binary data of the class, convert it to a class object, and store it in the cache. That's why you need to restart the JVM to take effect after changing the class.

Note: the parent-child relationship between class loaders is not the parent-child relationship on class inheritance, but the parent-child relationship between instances.

 

public class ClassloaderPropTest {
    public static void main(String[] args) throws IOException {
        //Get system classloader
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("System class loader:" + systemClassLoader);
        /*
        Get the loading path of the system classloader -- usually specified by the CLASSPATH environment variable, if not specified by the operating system
        CLASSPATH Environment variable, the current path is taken as the loading path of the system classloader by default
         */
        Enumeration<URL> eml = systemClassLoader.getResources("");
        while (eml.hasMoreElements()) {
            System.out.println(eml.nextElement());
        }
        //Get the system classloader's parent classloader and get the extension classloader
        ClassLoader extensionLoader = systemClassLoader.getParent();
        System.out.println("The parent loader of a system class is an extension class loader:" + extensionLoader);
        System.out.println("Load path of extension class loader:" + System.getProperty("java.ext.dirs"));
        System.out.println("Extension class loader's parant: " + extensionLoader.getParent());
    }
}
//Output result
//System class loader: sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//The parent loader of system class is extension class loader: sun.misc.Launcher$ExtClassLoader@1540e19d
//Load path of extension class loader: C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//Parallel of extension class loader: null

It is verified from the output that the parent loader of the system classloader is the extension classloader. But the parent loader of the extension class loader in the output is null. This is because the parent loader is not implemented in java, but in C + +, so it cannot be obtained. But the parent loader of the extension class loader is the root loader.

1.2 class loading flow chart

 

 

In the red part of the figure, we can customize the class loader to load.

5, Create and use a custom classloader

1. Custom class loading analysis

Except for the root class loader, all class loaders are subclasses of ClassLoader. So we can implement our own ClassLoader by inheriting ClassLoader.

ClassLoader class has two key methods:

  1. protected Class loadClass(String name, boolean resolve): name is the class name. If remove is true, the class will be resolved when loading.

  2. protected Class findClass(String name): finds a class according to the specified class name.

Therefore, if you want to implement a custom class, you can override these two methods. However, it is recommended to override the findClass method instead of the loadClass method, because the findClass method is called internally by the loadClass method.

Let's take a look at the source code of loadClass

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //The first step is to check whether it has been loaded from the cache
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                //Step 2: judge whether the parent loader is null
                    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) {
                   //Step 3: if none of the above is found, the findClass method will be called
                    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;
        }
    }

loadClass loading method flow:

  1. Determine whether this class has been loaded;

  2. If the parent loader is not null, the parent loader is used for loading; otherwise, the root loader is used for loading;

  3. If none of the previous loading is successful, use the findClass method to load.

Therefore, in order not to affect the class loading process, we can simply and conveniently implement the custom class loading by overriding the findClass method.

2. Implement custom class loader

Based on the above analysis, we simply override the findClass method to load a custom class.

public class Hello {
   public void test(String str){
       System.out.println(str);
   }
}

public class MyClassloader extends ClassLoader {

    /**
     * Read file contents
     *
     * @param fileName file name
     * @return
     */
    private byte[] getBytes(String fileName) throws IOException {
        File file = new File(fileName);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try (FileInputStream fin = new FileInputStream(file)) {
            //Read all binary data of Class file at one time
            int read = fin.read(raw);
            if (read != len) {
                throw new IOException("Unable to read all files");
            }
            return raw;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //Replace (.) of package path with slash (/)
        String fileStub = name.replace(".", "/");
        String classFileName = fileStub + ".class";
        File classFile = new File(classFileName);

        //If a Class file exists, the system is responsible for converting it to a Class object
        if (classFile.exists()) {
            try {
                //Read binary data of Class file into array
                byte[] raw = getBytes(classFileName);
                //Call the defineClass method of ClassLoader to convert binary data to Class object
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //If clazz is null, the load fails and an exception is thrown
        if (null == clazz) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        String classPath = "loader.Hello";
        MyClassloader myClassloader = new MyClassloader();
        Class<?> aClass = myClassloader.loadClass(classPath);
        Method main = aClass.getMethod("test", String.class);
        System.out.println(main);
        main.invoke(aClass.newInstance(), "Hello World");
    }
}
//Output result
//Hello World

ClassLoader also has an important method, defineClass(String name, byte[] b, int off, int len). The purpose of this method is to convert the binary array of class to a Calss object.

This example is very simple. I wrote a Hello test class and put it in the current path after compilation (you can add judgment in findClass. If you don't have this file, you can try to find the. java file and compile it to get the. Class file; or you can judge that the last update time of the. java file is greater than the last update time of the. Class file, and then recompile and other logic) .

Six, summary

This article starts from the three stages of class loading: loading, linking and initialization, and explains the process of each stage in detail. It also explains the differences and connections of the common class loaders used by the JVM, as well as the process of the class loading mechanism. Finally, it concludes this article with a user-defined class loader example. My brother's ability is limited. If you see any problems, please point out and let the blogger learn to correct them. Welcome to discuss.

18 original articles published, 10 praised, 30000 visitors+
Private letter follow

Posted by rachwilbraham on Fri, 28 Feb 2020 04:47:26 -0800