The loading process of classes in the Java virtual machine

The loading process of classes in Java

For example, the following simple code

public class HelloWorld {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("我已经被加载啦");
    }
}

What is its loading process like? The
Insert picture description here
complete process is shown in the figure below:
Insert picture description here
Loading phase

  • Obtain the binary byte stream that defines this class through the fully qualified name of a class
  • Convert the static storage structure represented by this byte stream into the runtime data structure of the method area
  • Generate a java.lang.Class object representing this class in memory as the access entry for various data of this class

How to load class files

  • Load directly from the local system
  • Obtained through the Internet, typical scenario: Web Applet
  • Read from the zip archive and become the basis of the jar and war formats in the future
  • Run-time calculation and generation, the most used is: dynamic proxy technology
  • Generated by other files, typical scenario: JSP application extracts .class files from a proprietary database, which is relatively rare
  • Obtained from encrypted files, typical protection measures against decompilation of Class files

Link phase

Verify

  • The purpose is to ensure that the information contained in the byte stream of the Class file meets the requirements of the current virtual machine, to ensure the correctness of the loaded class, and not to endanger the security of the virtual machine itself.
  • It mainly includes four verifications, file format verification, metadata verification, bytecode verification, and symbol reference verification .

Check the bytecode through the tool Binary Viewer.
Insert picture description here
If an illegal bytecode file appears, the verification will fail!

Prepare

Allocate memory for the class variable and set the default initial value of the class variable, that is, zero value, null, etc.

public class HelloWorld {
    
    
    private static int a = 1;  // 准备阶段为0,在下个阶段,也就是初始化的时候才是1
    public static void main(String[] args) {
    
    
        System.out.println(a);
    }
}
  • The variable a above will be assigned an initial value in the preparation phase, but it is not 1, but 0.
  • The static modified with final is not included here, because final will be allocated during compilation and will be explicitly initialized in the preparation phase.
  • The instance variable will not be allocated and initialized here, the class variable will be allocated in the method area, and the instance variable will be allocated to the Java heap along with the object.

Resolve

  • The process of converting symbol references in the constant pool into direct references.
  • In fact, the parsing operation is often accompanied by the JVM after the initialization is executed.
  • Symbol reference is a set of symbols to describe the referenced target. The literal form of symbol reference is clearly defined in the class file format of the "Java Virtual Machine Specification". A direct reference is a pointer that directly points to the target, a relative offset, or a handle that is indirectly located to the target.
  • The parsing action is mainly for classes or interfaces, fields, class methods, interface methods, method types, etc. Corresponding to CONSTANT Class info, CONSTANT Fieldref info, and Constant Methodref info in the constant pool.

Initialization phase

  • The initialization phase is the process of executing the class constructor method. This method does not need to be defined. It is the javac compiler that automatically collects the assignment actions of all class variables in the class and merges the statements in the static code block (that is, when our code contains static variables, there will be clinit method). The instructions in the constructor method are executed in the order in which the statements appear in the source file.
  • The class constructor is different from the class constructor. If there is no static variable, the class construction method (clinit) will not be executed, and after any class is declared, a constructor is generated, and the default is an empty parameter constructor. The constructor of the execution class is to execute the init method.
public class ClassInitTest {
    
    
    private static int num = 1;
    static {
    
    
        num = 2;
        number = 20;
        System.out.println(num);
        System.out.println(number);  //报错,非法的前向引用
    }

    private static int number = 10;

    public static void main(String[] args) {
    
    
        System.out.println(ClassInitTest.num); // 2
        System.out.println(ClassInitTest.number); // 10
    }
}

About the variable assignment process when the parent class is involved

If the class has a parent class, the JVM will ensure that the parent class has been executed before the execution of the child class.

public class ClinitTest1 {
    
    
    static class Father {
    
    
        public static int A = 1;
        static {
    
    
            A = 2;
        }
    }

    static class Son extends Father {
    
    
        public static int b = A;
    }

    public static void main(String[] args) {
    
    
        System.out.println(Son.b);
    }
}

The output result is 2, which means that when ClinitTest1 is loaded first, the main method will be found, and then the initialization of Son will be executed, but Son inherits Father, so the initialization of Father needs to be executed, and A is assigned to 2 at the same time. We get Father's loading process through decompilation. First, we see that the original value is assigned to 1, and then copied to 2, and finally returned.

iconst_1
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
return

The virtual machine must ensure that the initialization method of a class is synchronized and locked in multiple threads.

public class DeadThreadTest {
    
    
    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
            new DeadThread();
        }, "t1").start();

        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
            new DeadThread();
        }, "t2").start();
    }
}
class DeadThread {
    
    
    static {
    
    
        if (true) {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
            while(true) {
    
    

            }
        }
    }
}

The output of the above code is:

线程t1开始
线程t2开始
线程t2 初始化当前类

It can be seen from the above that after initialization, initialization can only be performed once, which is the process of synchronous locking.

Guess you like

Origin blog.csdn.net/qq_33626996/article/details/112829936