Thoroughly analyze the JVM class loading mechanism

This article is still based on the JDK8 version, with some changes from the JDK9 modularizer to the class loader.

0 javac compilation

java code

public class Math {
    public static final int initData = 666;

    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        System.out.println("end");
    }
}

javac compilation, javap -v -p view class file

Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class

// Part 1, description information: size, modification time, md5 value, etc
  Last modified 2022 January 8; size 1006 bytes
  MD5 checksum 4cece4543963b23a98cd219a59c1887c
  Compiled from "Math.java"

// Part 2, Description: compiled version
public class com.lzp.java.jvm.classloader.Math
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // com/lzp/java/jvm/classloader/Math
  super_class: #11                        // java/lang/Object
  interfaces: 0, fields: 2, methods: 4, attributes: 1
  
// Part 3, constant pool information
Constant pool:
   #1 = Methodref          #11.#39        // java/lang/Object."<init>":()V
   #2 = Class              #40            // com/lzp/java/jvm/classloader/Math
   #3 = Methodref          #2.#39         // com/lzp/java/jvm/classloader/Math."<init>":()V
   #4 = Methodref          #2.#41         // com/lzp/java/jvm/classloader/Math.compute:()I
   #5 = Fieldref           #42.#43        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #44            // end
   #7 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #47            // com/lzp/java/jvm/classloader/User
   #9 = Methodref          #8.#39         // com/lzp/java/jvm/classloader/User."<init>":()V
  #10 = Fieldref           #2.#48         // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
  #11 = Class              #49            // java/lang/Object
  #12 = Utf8               initData
  #13 = Utf8               I
  #14 = Utf8               ConstantValue
  #15 = Integer            666
  #16 = Utf8               user
  #17 = Utf8               Lcom/lzp/java/jvm/classloader/User;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               Lcom/lzp/java/jvm/classloader/Math;
  #25 = Utf8               compute
  #26 = Utf8               ()I
  #27 = Utf8               a
  #28 = Utf8               b
  #29 = Utf8               c
  #30 = Utf8               main
  #31 = Utf8               ([Ljava/lang/String;)V
  #32 = Utf8               args
  #33 = Utf8               [Ljava/lang/String;
  #34 = Utf8               math
  #35 = Utf8               MethodParameters
  #36 = Utf8               <clinit>
  #37 = Utf8               SourceFile
  #38 = Utf8               Math.java
  #39 = NameAndType        #18:#19        // "<init>":()V
  #40 = Utf8               com/lzp/java/jvm/classloader/Math
  #41 = NameAndType        #25:#26        // compute:()I
  #42 = Class              #50            // java/lang/System
  #43 = NameAndType        #51:#52        // out:Ljava/io/PrintStream;
  #44 = Utf8               end
  #45 = Class              #53            // java/io/PrintStream
  #46 = NameAndType        #54:#55        // println:(Ljava/lang/String;)V
  #47 = Utf8               com/lzp/java/jvm/classloader/User
  #48 = NameAndType        #16:#17        // user:Lcom/lzp/java/jvm/classloader/User;
  #49 = Utf8               java/lang/Object
  #50 = Utf8               java/lang/System
  #51 = Utf8               out
  #52 = Utf8               Ljava/io/PrintStream;
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               println
  #55 = Utf8               (Ljava/lang/String;)V
{
// The fourth part is variable information
  public static final int initData;
    descriptor: I
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 666

  public static com.lzp.java.jvm.classloader.User user;
    descriptor: Lcom/lzp/java/jvm/classloader/User;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

  public com.lzp.java.jvm.classloader.Math();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lzp/java/jvm/classloader/Math;
// Part V, method information
  public int compute();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 4
        line 12: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/lzp/java/jvm/classloader/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lzp/java/jvm/classloader/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: ldc           #6                  // String end
        18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        21: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 13
        line 19: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  args   [Ljava/lang/String;
            8      14     1  math   Lcom/lzp/java/jvm/classloader/Math;
    MethodParameters:
      Name                           Flags
      args

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class com/lzp/java/jvm/classloader/User
         3: dup
         4: invokespecial #9                  // Method com/lzp/java/jvm/classloader/User."<init>":()V
         7: putstatic     #10                 // Field user:Lcom/lzp/java/jvm/classloader/User;
        10: return
      LineNumberTable:
        line 6: 0
}

Method, you can find the corresponding symbol in ConstantPool.

Refer to bytecode instruction table: https://docs.oracle.com/javase/specs/jvms/se8/html/index.html.

Class 1 loading process

j the classic class loading process is shown in the following figure, including loading, linking and initialization.

1.1 loading class files

The bytecode file is located on the disk. When a Class is used (for example, calling the main() method and new object), find and read the binary stream of the file in the disk through IO, convert it into the data structure of the method area, store it in the method area, and generate Java in the Java heap Lang. Class object. The Class object is an access portal to the method area. It is used for java reflection mechanism to obtain various information of the Class.

1.2 linking process

Verify: verify whether the class file conforms to the specification

  1. Verification of file format. Verify whether it starts with 0XCAFEBABE and whether the version number is reasonable

  2. Metadata validation. Whether there is a parent class, whether it inherits the final class (the final class cannot be inherited), and whether the non abstract class implements all abstract methods.

  3. Bytecode verification. (omitted)

  4. Symbol reference validation. The constant pool describes whether the class exists and whether the accessed methods or fields exist and have sufficient permissions.

-Xverify:none  // Cancel validation

Preparation: allocate memory for static variables of the class and initialize to the initial value of the system

Variables modified by final static: directly assign values to user-defined values, such as private final static int value=123 and directly assign 123.

private static int value=123, the value of this stage is still 0.

Parsing: converting symbolic references to direct references (static links)

Each method and method parameter in Java code are symbols, and the class is loaded into the Constant pool in the method area.

Symbolic reference: it should be understood as these literal quantities in the constant pool. [may not understand, right]

Direct reference: the location (pointer, handle) where the symbol corresponding code is loaded into JVM memory.

The static linking process is completed when the class is loaded. It mainly converts some static methods. Dynamic linking is done during program running, replacing symbolic references with direct references.

1.3 initialization (class initialization clinit -- > init)

Execute the < clinit > method. The clinit method is formed by the compiler automatically collecting the assignment actions of all static variables in the class and combining the static statement blocks, also known as the class constructor method.

  • The initialization sequence is consistent with that in the source file

  • The < clinit > of the parent class will be called before the < clinit > of the child class is called

  • The JVM guarantees thread safety of the clinit method

During initialization, if a new object is instantiated, the < init > method will be called to initialize the instance variable / code block and execute the code in the corresponding construction method.

The class loading process is lazy, and it will be loaded only when it is used.

Initialization example

public class JVMTest2 {
    static {
        System.out.println("JVMTest2 Static block");
    }

    {
        System.out.println("JVMTest2 Tectonic block");
    }

    public JVMTest2() {
        System.out.println("JVMTest2 Construction method");
    }

    public static void main(String[] args) {
        System.out.println("main method");
        new Sub();
    }
}

class Super {
    static {
        System.out.println("Super Static code block");
    }

    public Super() {
        System.out.println("Super Construction method");
    }

    {
        System.out.println("Super Common code block");
    }
}

class Sub extends Super {
    static {
        System.out.println("Sub Static code block");
    }

    public Sub() {
        System.out.println("Sub Construction method");
    }

    {
        System.out.println("Sub Common code block");
    }
}

JVMTest2 Static block
main method
Super Static code block
Sub Static code block
Super Common code block
Super Construction method
Sub Common code block
Sub Construction method

To execute the main method, you do not need to create a JVMTest2 instance.

Type 2 loader

View the current JDK class loader

public class PrintJDKClassLoader {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println(parent);
        ClassLoader parentParent = parent.getParent();
        System.out.println(parentParent);
    }
}

// JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@28a418fc
null
// JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
null

Class 2.1 loader (JDK8)

Class loader initialization process: Java calls JVM DLL file to create a JVM, create a boot class loader (implemented by C + +), and load the extension class loader and application class loader through the JVM launcher (sun.misc.Launcher).

  • Start the class loader: it is responsible for loading the core class library under the lib directory. As a part of the JVM, it is implemented by C + +.

  • Extension class / platform class loader: it is responsible for loading JAR class packages in ext extension directory under lib directory.

  • Application class loader: it is responsible for loading the class package under the ClassPath of the user's class path, mainly loading the classes written by the user.

  • Custom class loader: it is responsible for loading the class package under the user-defined path.

By default, the JVM loads our application using the instance of the class loader AppClassLoader returned by the Launcher's getClassLoader() method.

// Launcher construction method
public Launcher() {
    Launcher.ExtClassLoader var1;
    // Construct an extension class loader and set the class loader parent property to null.
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
    // Construct the application class loader and set the parent attribute of the class loader as the extended class loader.
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    Thread.currentThread().setContextClassLoader(this.loader);
    // Permission verification code
    }
}

2.2 parental delegation model

The class loader adopts a three-tier, parental delegation model. The parent-child relationship of the class loader is not an inheritance relationship, but a combination relationship. Except for the startup class loader, other class loaders inherit from the ClassLoader class.

Working process: when the class loader receives a class loading request, it first determines whether the class has been loaded. If it has not been loaded, it attempts to delegate the request upward to the parent class loader for loading. When the parent class loader fails to complete the loading task, the child class loader attempts to load.

// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // Check whether the class has been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) { 
                    // Non boot class loader
                    c = parent.loadClass(name, false);
                } else { 
                    // Start class loader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // The parent class loader cannot load the specified class
            }

            if (c == null) {
                // Call the findClass method of the current class loader to load the class
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

Why does it feel like a detour to use the parental delegation model?

Under the parental delegation model, class loading requests are always delegated to the top-level startup class loader. For unloaded classes, you need to go from the bottom to the top; If the user-defined class has been loaded, the delegation process is not required.

Using the parental delegation mechanism has the following advantages:

  • Sandbox security mechanism to prevent the core class library code from being tampered with.
  • Avoid repeated class loading. If the parent class loader is loaded, the child class loader does not need to be loaded again.

Overall responsibility entrustment mechanism

Overall responsibility: that is, when a classloader loads a Class, other classes that the Class depends on and references are usually loaded by the classloader. Delegation mechanism: let the parent (parent) Class loader look for it first, and look for it from its own Class path only when the parent cannot be found.

Refer to Launcher construction method

Thread.currentThread().setContextClassLoader(this.loader);

Custom class loader

The user-defined class loader mainly inherits the ClassLoader class and overrides the findClass(name) method in the above source code.

public class CustomClassLoaderTest {
    static class CustomClassLoader extends ClassLoader {
        private String classFilePath;

        public CustomClassLoader(String classFilePath) {
            this.classFilePath = classFilePath;
        }
		// Load class data stream
        private byte[] loadClassFile(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classFilePath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException{
            try {
                byte[] data = loadClassFile(name);
                // Load link initialization logic
                return defineClass(name,data,0,data.length);
            } catch (Exception e) {
                throw new ClassNotFoundException();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        CustomClassLoader classLoader = new CustomClassLoader("F:");
        Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
        Object instance = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("add", null);
        System.out.println(method.invoke(instance));
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

The parent loader of the custom class loader is the application class loader. CustomClassLoader runs using AppClassLoader and is naturally a parent class loader.

Break the parental delegation mechanism

In some scenarios, breaking parental delegation is necessary. For example, there may be multiple applications in Tomcat that refer to different Spring versions. Breaking parental delegation can realize application isolation.

The JVM uses the loadClass method to implement the parental delegation mechanism. Override the loadClass method to break the parental delegation mechanism.

It is not feasible to directly delete the parent delegation code. Java code inherits from Object and always needs parent delegation to load the core code.

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // Non custom classes are loaded by parental delegation
            if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
                c = this.getParent().loadClass(name);
            } else {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

Note: the core library code of JDK is not allowed to be configured and modified by itself. For example, you cannot use object Class copy out and execute. Sandbox isolation.

Keywords: jvm

Added by sobbayi on Sun, 09 Jan 2022 13:49:35 +0200