Java虚拟机动态加载、链接和初始化类和接口。

  • Loading:加载是查找具有特定名称的类或接口类型的二进制表示,并根据该二进制表示创建类或接口的过程。
  • Linking:链接是获取类或接口并将其组合到Java虚拟机的运行时状态以便执行的过程。
  • Initializing:类或接口的初始化包括执行类或接口初始化方法

    类生命周期

02 Loading、Linking、Initializing - 图1

Loading 加载

类加载的第一个阶段是加载(Loading),在此阶段,JVM完成3件事:

  1. 通过一个类的全限定名来获取定义的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区对这个类的各种数据的访问入口

加载阶段,是读取类的二进制流的过程,可以通过JVM 内置的Bootstrap Class Loader 完成,也可以由自定义的Class Loader完成。自定义一个ClassLoader需要重写抽象类ClassLoader的findClass()或loadClass()。

Loading 加载过程,只能加载非数组类,数组类是有JVM直接在内存中构造出来。

  • 对于基础类型数组,JVM会把数组与Bootstrap 启动类关联起来
  • 对于引用类型数组,JVM会把该数组表示为加载该类型的类加载器的类名称空间上。

    ClassLoader

    类与类加载器的关系

    对于一个类,都必须由它的类加载器和这个类本身一起共同确定其在JVM中的唯一性,每一个类加载器,都拥有一个类名称空间。(同一个类的字节码文件,被不同类加载器加载,两个类就不同) ```java /**
  • 验证类与类加载器的关系
  • 同一个类的class文件,由两个不同的类加载器加载,那么两个类也是 不同的类
  • @author :Administrator
  • @date :2022/4/24 0024 */ public class Code01_ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

    1. // 自定义的类加载器
    2. ClassLoader myClassLoader = new ClassLoader() {
    3. @Override
    4. public Class<?> loadClass(String name) throws ClassNotFoundException {
    5. String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    6. InputStream is = getClass().getResourceAsStream(fileName);
    7. if (Objects.isNull(is)) {
    8. return super.loadClass(name);
    9. } else {
    10. byte[] bytes = null;
    11. try {
    12. bytes = new byte[is.available()];
    13. is.read(bytes);
    14. } catch (IOException e) {
    15. e.printStackTrace();
    16. }
    17. assert bytes != null;
    18. return defineClass(name, bytes, 0, bytes.length);
    19. }
    20. }
    21. };
    22. Object object = myClassLoader.loadClass("com.ixiaoyu2.jvm.classloader.Code01_ClassLoaderTest").newInstance();
    23. System.out.println(object.getClass());
    24. System.out.println(object instanceof Code01_ClassLoaderTest);

    } } ```

    JVM类加载器的三种特性

    全盘负责

    当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

    父类委托(双亲委派)

    是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。(作为加载入口)

    缓存机制

    缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区

    双亲委派机制

    JVM规范里指出,只有2种类加载器,一类是Bootstrap ClassLoader,由C++实现;另一类是用户类加载器,由用户继承java.lang.ClassLoader实现。JDK1.2以来,java保持着三层类加载器、双亲委派的类加载结构:

  • Bootstrap ClassLoader 启动类加载器
    • Bootstrap ClassLoader 负载加载%JAVA_HOME%\lib目录下,或被-Xbootclasspath参数所指定的路径中下,存放的而且是JVM能够识别(安装文件名识别,如rt.jar、tools.jar,名字不符合的类库不会被加载)的类库。
    • Bootstrap ClassLoader是由C++实现,无法被java程序直接引用,使用getClassLoader返回null。
  • Extension ClassLoader 扩展类加载器
    • Extension ClassLoader负载加载%JAVA_HOME%\lib\ext目录下,或者被java.ext.dirs系统变量所指定的路径下的所有类库。
  • Application ClassLoader 应用类加载器
    • Application ClassLoader 负载加载用户路径(classpath)下所有的类库。
    • Application ClassLoader 是ClassLoader#getSystemClassLoader()的返回值,所以有时Application ClassLoader也被称为“系统类加载器”
  • Custom ClassLoader 用户自定义加载器
    • 用户可继承抽象类ClassLoader 实现findClass()或loadClass()方法,自定义类加载器。
/**
 * 获取类加载器
 *
 * @author :Administrator
 * @date :2022/4/24 0024
 */
public class Code02_GetClassLoaderTest {

    public static void main(String[] args) {
        // Bootstrap ClassLoader加载核心类 %JAVA_HOME%\jre\lib目录下的类库,无法直接获取
        System.out.println(ComponentBeanInfo.class.getClassLoader());
        System.out.println(String.class.getClassLoader());
        // Extension ClassLoader加载%JAVA_HOME%\jre\lib\ext目录下的类库
        System.out.println(CollationData_ar.class.getClassLoader());
        // Application ClassLoader 加载classpath下的类
        System.out.println(Code02_GetClassLoaderTest.class.getClassLoader());

        // 自定义的类加载器
        Code00_MyClassLoader myClassLoader = new Code00_MyClassLoader();
        Object o = null;
        try {
            o = myClassLoader.loadClass("com.ixiaoyu2.jvm.classloader.Code02_GetClassLoaderTest").newInstance();
        } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) {
            e.printStackTrace();
        }
        assert o != null;
        System.out.println(o.getClass().getClassLoader());
    }
}

image.png

类加载器关系

02 Loading、Linking、Initializing - 图3

public class Code04_ChildAndParentClassLoader {
    public static void main(String[] args) {
        //App
        System.out.println(Code04_ChildAndParentClassLoader.class.getClassLoader());
        //Ext  App的父加载器是Ext
        System.out.println(Code04_ChildAndParentClassLoader.class.getClassLoader().getParent());
        //null  Ext的父加载器是Bootstrap
        System.out.println(Code04_ChildAndParentClassLoader.class.getClassLoader().getParent().getParent());
        //null
        System.out.println(Code04_ChildAndParentClassLoader.class.getClassLoader().getClass().getClassLoader());
    }
}

类加载过程

02 Loading、Linking、Initializing - 图4

使用双亲委派进制进行类的加载,其主要目的是为了安全。其次可减少相同类型的类被重复加载。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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;
    }
}

破坏双亲委派机制

  1. jdk1.2之前
  2. java SPI(Service Provider Interface)加载,采用Thread Context ClassLoader
  3. 热部署(OSGI实现模块热部署)

    Linking 链接

    链接类或接口需要验证和准备类或接口、它的直接父类、它的直接接口和它的元素类型(如果它是数组类型)。解析类或接口中的符号引用是链接的可选部分。

    Verification 验证

    验证是保证Class字节流包含的信息符合JVM规范的全部约束条件,验证阶段分为四个阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。

  4. 文件格式验证

验证字节流是否符合Class文件格式规范,并且能被当前版本虚拟机处理。

  1. 元数据验证

对字节码描述的信息进行语义分析,以保证其描述符合JVM规范

  1. 字节码验证

字节码验证的主要目的是通过对数据流、控制流的分析,确保程序语义是合法、符合逻辑的。主要是对类的方法体(Class文件中的Code属性)进行校验分析,以保证校验类中的方法在运行时不会做出危害虚拟机的安全行为。

  1. 符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段发生。符号引用验证主要目的是确保解析行为能正常执行。

注:可使用 -Xverify:none 取消验证

Preparation 准备

在准备阶段,为类中定义的变量(静态变量)分配内存,通常情况下设置为默认值。这些变量存储在方法区中,jdk1.7之前,方法区的实现为Permanent Generation,jdk8以后为MetaSpace。如果字段的字段属性表中存在ConstantValue属性,那么在Preparation 准备阶段会把字段赋予ConstantValue所指定的值。

Resolution 解析

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。对类、接口、字段、方法、接口方进行解析,将符号引用转换为可直接指向目标的指针、相对偏移量或者一个可以间接定位到目标的句柄的字节引用。

同一符号引用进行多次解析很常见,除了invokedynamic指令外,虚拟机可以对第一次解析结果进行缓存,来避免解析动作的重复进行。

Initialization 初始化

初始化阶段就是执行类构造器()方法的过程。

  • ()方法是由编译器自动类中所有类变量的赋值动作和静态语句块中的语句合并产生,收集顺序由源码中的顺序决定。静态语句中只能访问定义在静态语句块之前的变量,静态语句块之后的变量可以赋值但是静态语句块不能访问。
  • ()不需要显示调用父类构造器,JVM保证在子类()调用时,父类的()方法已经执行完毕。
  • 父类的()方法先执行,因此父类的静态语句块优先于子类变量赋值操作。

    JVM严格规定6中需要立即初始化的情况:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果没有进行过初始化。则需要先对其初始化。
    1. 使用new关键字实例化对象的时候
    2. 读取或设置一个类型的静态字段(被final修饰、已在编译期间放入常量池的字段除外)
    3. 调用一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类型没有进行初始化,需要先进性初始化
  3. 当初始化类时,如果其父类没有初始化,先对其父类进行初始化
  4. 虚拟机启动时,指定一个需要执行的主类,虚拟机会先初始化这个主类
  5. jdk1.7加入动态语言支持,如果java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种方法句柄,并且方法句柄对应的类没有进行过初始化,则需要对其进行初始化
  6. jdk1.8接口中定义的默认方法,如果这个接口的实现类进行了初始化,那么这个接口需要在实现类之前进行初始化。 ```java /**
    • 测试类加载过程
    • @author :Administrator
    • @date :2022/4/25 0025 */ public class Code07_ClassLoadingProcedure { public static void main(String[] args) { System.out.println(T.count); // 调用T的静态字段,T进行初始化 打印结果2 } }

class T { public static T t = new T(); public static int count = 2;

private T() {
    count++;
}

}

```java
Compiled from "Code07_ClassLoadingProcedure.java"
public class com.ixiaoyu2.jvm.classloader.Code07_ClassLoadingProcedure {
  public com.ixiaoyu2.jvm.classloader.Code07_ClassLoadingProcedure();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2     获取System.out             // Field java/lang/System.out:Ljava/io/PrintStream;
       3: getstatic     #3     获取T.count  2             // Field com/ixiaoyu2/jvm/classloader/T.count:I
       6: invokevirtual #4     println方法             // Method java/io/PrintStream.println:(I)V
       9: return
}


0: new           #3        创建一个对象          // class com/ixiaoyu2/jvm/classloader/T
3: dup                     复制一份放入栈顶
4: invokespecial #4        调用构造方法<init>          // Method "<init>":()V
7: putstatic     #5        将count设为1          // Field t:Lcom/ixiaoyu2/jvm/classloader/T;
10: iconst_2               将整型常量2压入栈中
11: putstatic     #2       将2付给count           // Field count:I




开辟一个新的栈帧frame

 0 aload_0   将this压栈
 1 invokespecial #1 <java/lang/Object.<init> : ()V>   弹出this,调用父类构造方法
 4 getstatic #2 <com/ixiaoyu2/jvm/classloader/T.count : I>  获取T.ount的值 (默认值0),压栈
 7 iconst_1 整型常量1 压栈 
 8 iadd  T.count与1相加,结果压栈
 9 putstatic #2 <com/ixiaoyu2/jvm/classloader/T.count : I>  将T.count设置相加结果1
12 return 返回