jvm9-class loading mechanism

overview

  • The virtual machine loads the data describing the class from the class file to the memory, and verifies, parses and initializes the data, and finally forms a java type that can be directly used by the virtual machine. This is the class loading mechanism of the virtual machine

  • Equivalent to lazy loading mechanism

OSGI is implemented through the class loading mechanism

class loading time

load:

connect:

initialization:

Initialization will only be performed in the following cases

1. When encountering the four bytecode instructions of new, getstatic, putstatic or invokestatic, if the class has not been initialized, it needs to be initialized first. The most common java code scenarios for generating these 4 instructions are: when using the new keyword is an pear object, reading or setting a static field of a class (decorated by final, and the compiler has put the result into the constant pool static fields), and when calling a static method of a class.

2. When using the method of the java.lang.reflect package to make a reflective call to the class, if the class has not been initialized, you need to trigger its initialization first.

3. When initializing a class, if you find that its parent class has not been initialized, you need to trigger the initialization of its parent class first

4. When the virtual machine starts, the user needs to specify a main class to be executed (the class containing the main() method), and the virtual machine first initializes the main class.

5. jdk7 began to support dynamic languages,

Example of not being initialized

1. Refer to the static fields of the parent class through the subclass, and the subclass will not be initialized

2. Reference the class through the array definition

3. Call the constant of the class

use:

Uninstall:

class loading

The process of loading and connecting is parallel

load

  • Get the binary stream defining this class by its fully qualified name

Load source: can be downloaded from

files (class files, jar files),

network(?),

Calculate and generate a binary stream (proxy mode-proxy),

Other files are generated, such as Jsp files,

database

waiting to load

  • Convert the static storage structure represented by this byte stream into the runtime data structure of the method area

hotspot will store the loaded class object in the method area

  • Generate a Class object representing this class in memory as an access entry for various data of this class

connect

verify
  • Verification is the first step of connection. The purpose of this stage is to ensure that the information contained in the byte stream of the class file meets the requirements of the current virtual machine and will not endanger the safety of the virtual machine itself.

Mainly check the following 4 categories

  • File Format Validation

  • metadata validation

  • bytecode verification

  • 符号引用验证:主要为了确保解析环节正确

(nosuchmetch等异常就是这里抛出的)

准备
  • 准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配

  • 这里的初始值并非我们指定的值,而是其默认值,但是如果被final修饰,那么在这个过程中,常量值会被一同指定

int 0

boolean false

float 0.0

char '0'

抽象数据类型 null

class hello{

// 准备阶段会把a初始化为默认的值0

public static int a = 10;

}
解析

解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。

到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为解析状态),这样就避免了一个符号引用的多次解析

解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。这里主要说明前4种的解析过程

类或者接口解析

要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤

1、如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器区加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载。
2、如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似“java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer,接着虚拟机将会生成一个代表此数组对象的直接引用。
3、如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.IllegalAccess异常。
字段解析

对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤

1、如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
2、否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么就直接返回这个字段的直接引用,解析结束
3、否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
4、否则,解析失败,抛出java.lang.NoSuchFieldError异常

如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常

类方法解析

进行类方法的解析仍然需要先解析此类,在正确解析之后需要进行如下的步骤:

1、类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
2、如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
3、否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
4、否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethmodError异常
5、否则,查找失败,抛出java.lang.NoSuchMethodError异常

如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常

接口方法解析

同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:

1、如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
2、否则在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用
3、否则,在该接口以及其父接口中查找,直到Object类,如果找到这直接返回这个方法的直接引用
4、否则,查找失败

接口的所有方法都是public,所以不存在访问权限问题,不需要进行权限验证

初始化(面试题比较多需要关注)

类初始化是类加载的最后一步,前面类加载的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与以外,其余动作完全由虚拟机主导与控制。到了初始化阶段,才是真正执行类中定义的java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源(初始化阶段是执行类构造器 ()方法的过程)
先来看一段代码

public class Demo{
    static{
        i = 0;
        // 编译报错:illegal forward reference
        System.out.println(i);
    }
    static int i = 1;
}

上面这段代码变量的赋值语句可以通过编译,而下面的输出确编译不通过,这是为什呢?

()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块中可以赋值,但是不能访问

再来看一段代码

public class Parent{
  public static int A =1;
  static {
    A = 2;
  }
  
  static class Sub extents Parent{
    public static int B = A;
  }
  
  public static void main(){
    System.out.println(Sub.B);
  }
}
  • 子类的 ()在执行之前,虚拟机保证父类的先执行完毕,因此在赋值前父类static已经执行,因此结果为2。

  • 接口中也有变量要赋值,也会生成 ()但不需要先执行父类的 ()方法。只有父接口中定义的变量使用时才会初始化。

  • 如果多个线程同时初始化一个类,只有一个线程执行这个类的 ()方法,其他线程等待执行完毕。如果方法执行时间过长,那么就会造成多个线程阻塞

    public class DemoThread {
    
        static class Hello{
            static{
                System.out.println(Thread.currentThread().getName() + "init ...");
                try{
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "start ...");
                    Hello he = new Hello();
                    System.out.println(Thread.currentThread().getName() + " end ...");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "start ...");
                    Hello he = new Hello();
                    System.out.println(Thread.currentThread().getName() + " end ...");
                }
            }).start();
        }
    }

    输出结果:

    Thread-0start ...
    Thread-1start ...
    Thread-0init ...
    Thread-0 end ...
    Thread-1 end ...

类加载器

  • 虚拟机的设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为类加载器。
  • 只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等
类加载器分类

从虚拟机的角度只分为启动类加载器和其它加载器
从使用者角度分为:

  • 启动类加载器
    由c++实现,是虚拟机的一部分,用于加载javahome下的lib目录下的类
  • 扩展类加载器
    加载javahome下/lib/ext目录中的类
  • 应用程序类加载器(系统类加载器)
    加载用户类路径上的所指定的类库
  • 自定义类加载器
自定义类加载器
  • 定义一个类,继承ClassLoader
  • 重写loadClass方法
  • 实例化Class对象
自定义类加载器的优势
  • 类加载器是java语言的一项创新,也是java语言流行的重要原因之一,它最初的设计是为了满足java Applet的需求而开发出来的。
  • 高度的灵活性
  • 通过自定义类加载器可以实现热部署
  • 代码加密

双亲委派模型

  • 从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
    1、如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
    2、每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
    3、如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
  • 双亲委派模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。

双亲委派模型的实现

ClassLoad.loadClass(String name, boolean resolve)

Guess you like

Origin blog.csdn.net/m0_53121042/article/details/111571137