JVM(二)类加载器子系统和类加载过程

类加载器子系统和类加载过程

1 简介

image-20221212194216072

类加载器子系统:负责从文件系统或者网络中加载字节码文件,字节码文件在文件开头有特定的文件标识(Coffee Baby)。ClassLoader只负责文件的加载,它是否可以运行由Execution Engine决定。

加载的类信息存放在内存中一块成为方法区的空间,除了类的信息之外,还会存放运行时常量池信息,还可能包括字符串字面量数字常量。(这部分常量信息是Class文件中常量池部分的内存映射

image-20221212195215980

如上图,类加载器主要作用是①将字节码文件加载到内存的方法区,加载到方法区的class file被称为DNA元数据模板,然后后续②JVM会调用getClassLoader()方法获取加载这个字节码文件的类加载器,然后③类加载器调用类的构造方法创建实例放到堆区。类加载器在.class -> JVM -> 方法区中的元数据模板中就充当了一个运输工具的作用。

类的加载过程

image-20221212195826010

2 类的加载过程

2.1 Loading 加载
  • 通过类的全限定名获取定义这个类的二进制字节流
  • 将字节流代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,对应第一步的二进制字节流,作为方法区中这个类的各种数据的访问入口
2.2 link链接

链接阶段主要分为三部分:

验证(Verify):目的在于确保Class字节码文件对应字节流的信息符合JVM的要求,保证加载类的正确性,不会危害虚拟机自身的安全(防止字节码拦截),主要包括四种验证:文件格式验证(JVM规范的字节码开头是CA FE BA BE)、元数据验证、字节码验证、符号引用验证。

准备(Prepare)

  • 类变量分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含final修饰的static变量,因为final修饰后就变成常量了,会在编译的时候就进行分配了,准备阶段只会显示初始化
  • 这里不会为实例变量进行初始化,类变量会分配在方法区,而实例变量则是会随着对象一起分配到Java堆里面

类变量即类中的静态变量,实例变量是类中没有static修饰的变量,也称为成员变量

由上可知:

  • 类变量和类的信息都存放在方法区,实例变量和对象存在于堆区,因此类变量随着类的消失而消失,实例变量随着对象的消失而消失

解析(Resolve)

  • 将常量池中的符号引用转换为直接引用:符号引用即程序导入的外部类库,外部类库如果全部放入字节码文件就太大了,所以只放入符号引用

    符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。

2.3 初始化initialization:
  • 初始化就是执行类构造器方法<clinit>()的过程,此方法不需要定义,是前端编译器javac自动收集类中的所有类变量的赋值动作静态代码块中的语句合并而来的。
  • 构造器方法中的指令按照语句在源文件出现的顺序执行
  • <clnit>()不同于类的构造器。<clinit>()是虚拟机视角下的初始化,<init>是实例化类时调用的方法,对非静态变量解析初始化,而<clinit>是类在初始化时调用的方法,是class类构造器对静态变量,静态代码块进行初始化
image-20221213105158886
  • 若该类具有父类,JVM会保证子类的<Clinit>执行之前,父类的<clinit>已经执行完毕
image-20221213112400097
  • 虚拟机必须保证一个类的clinit<>()方法在多线程下被同步加锁,即一个类只会被加载一次
public class DeadThreadTest {

    public static void main(String[] args) {
        Runnable r = () -> {
          System.out.println(Thread.currentThread().getName() + "开始");
          DeadThread deadThread = new DeadThread();
          System.out.println(Thread.currentThread().getName() + "结束");
        };

        Thread t1 = new Thread(r, "线程1");
        Thread t2 = new Thread(r, "线程2");

        t1.start();
        t2.start();
    }
}

class DeadThread {
    static {
        if(true) {
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            while(true) {

            }
        }
    }
}

输出:

线程1开始
线程2开始
线程1初始化当前类

静态代码块的输出语句只输出了一次,而静态代码块是<clinit>()方法在类初始化的时候执行的,所以上面测试可以验证:类只会被加载一次,而且多线程初始化类的时候会对类的clinit方法进行加锁操作

3 类加载器的分类

JVM支持两种类型的类加载器:引导类加载器和自定义加载器,其中所有派生于抽象类ClassLoader的一类加载器都是自定义加载器

image-20221213152907383

不管怎么划分,至少需要知道三类加载器:引导类加载器、扩展类加载器、系统类加载器,后面的两个类都间接继承了ClassLoader所以都属于自定义加载器,而且都是java语言编写的,引导类加载器是c c++编写的。

上图四者的关系是包含关系,不是上下层,也不是子父类的继承关系。

获取加载器测试

        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2

        // 获取上层扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1b6d3586

        // 获取上层引导类加载器,发现获取不到
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println(bootStrapClassLoader); //null

对于用户自定义的类来说,是由系统类加载器加载的

        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

发现其类加载器与系统类加载器类型相同,对象地址也相同:说明是同一个系统类加载器实例对象

对于核心类库,是由引导类加载器加载的:

        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1); // null

发现和引导类加载器一样获取不到

3.1 引导类加载器
  • 引导类加载器是使用C/C++实现的,嵌套在JVM内部。

  • 用来加载Java的核心类库,(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类

  • 引导类加载器并不继承自CLassLoader,没有父类加载器

  • 其他的两类加载器 扩展类加载器 和 应用程序类加载器 都属于核心类库,也是由引导类加载器加载的,并为他们指定父类加载器

  • 处于安全的考虑,引导类加载器只加载包名为java、Javax、sun等开头的类。

    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/resources.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/rt.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/sunrsasign.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jsse.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jce.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/charsets.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jfr.jar
    // file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/classes
    public static void main(String[] args) {
        // 获取bootstrap能够加载的api路径
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for(URL element : urls) {
            System.out.println(element.toExternalForm());
        }
    }

虽然获取不到引导类加载器,但是能够得到引导类加载器加载的类的路径,对上面路径下的class获取它的类加载器,会发现都是引导类加载器。

3.2 扩展类加载器
  • 扩展类加载器是由Java语言编写的,由sun.misc.Launcher$ExtClassLoader,Launcher的一个内部类实现的
  • 派生于ClassLoader
  • 父类加载器为启动类加载器
  • 从 java.ext.dirs 系统属性指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户自定义的JAR放在这个目录下,就会由扩展类加载器加载。
        String extDirs = System.getProperty("java.ext.dirs");
        for(String path : extDirs.split(";")) {
            System.out.println(path);
        }

        ClassLoader classLoader = CurveDB.class.getClassLoader();
        System.out.println(classLoader);
3.3 应用程序 、系统类加载器
  • 系统类加载器是由Java语言编写的,由sun.misc.Launcher$AppClassLoader,Launcher的一个内部类实现的
  • 派生于ClassLoader
  • 父类加载器为启动类加载器
  • 从 java.class.path 系统属性指定的目录加载 类库环境变量
  • 该类加载器是系统默认的加载器,一般来说,Java应用的类都是由它来完成加载的
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到加载该类的系统加载器
3.4 用户自定义加载器

除了上面三种类加载器相互配合工作,用户还可以自定义类加载器,来定制类的加载方式。

为什么要使用自定义加载器?

  • 隔离加载类

    在引入多个框架的时候,框架之间可能会依赖相同的jar包,这时候就需要进行类的仲裁,通过不同的自定义加载器使得各个中间件是隔离的,避免类的冲突

  • 修改类的加载方式:指在需要类的时候进行动态地加载类

  • 扩展加载源:除了之前的文件系统、网络中获取字节码文件,也可以扩展这个加载的来源。

  • 防止源码泄露,自定义加载器在运行的时候可以对加密的代码进行解密

定义自定义类加载器的步骤:

  1. 通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  2. 在JDK1.2之前,自定义类加载器需要继承ClassLoader并重写loadClass方法,从而实现自定义的类加载器,JDK1.2之后则不建议这么做了,而是直接把类加载逻辑写到findClass()里面
  3. 在编写自定义类加载器的时候,如果没有太过于复杂的需求,则可以直接继承UrlClassLoader类,这样就可以避免自己去编写findClass()以及获取字节码流的方式,使自定义类加载器更加简洁

4 ClassLoader

image-20221213220232552

ClassLoader是一个抽象类,但里面的方法不都是抽象方法,通常通过loadClass()或者findClass()和defineClass()组合搭配的方式来加载类。

4.1 获取ClassLoader的方式
  1. 获取当前类的classLoader:clazz.getClassLoader()
  2. 获取线程上下文的classLoader:Thread.currentThread().getContextClassLoader()
  3. 获取系统的classLoader:ClassLoader.getSystemClassLoader()
  4. 获取调用者的classLoader:DriverManager.getCallerClassLoader()

5 双亲委派机制

Java虚拟机对class文件采取的是按需加载的方式,即只有当需要使用该类的时候才将类的class文件加载到内存生成class对象。而且当加载某个class文件的时候,Java虚拟机采用的是双亲委派模式这样一种任务委派模式,即把请求交由父类处理。

工作原理

如果一个类加载器收到了类加载请求,它不会自己先去加载,而是会先把这个请求委托给父类去执行。如果父类加载器还存在父类,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回,否则子类才会尝试自己去加载,这就是双亲委派模式。

image-20221213223633193
双亲委派机制举例一
public class StringTest {
    public static void main(String[] args) {
        String string = new String("Hello!");
        System.out.println(string);


        StringTest stringTest = new StringTest();
        System.out.println(stringTest.getClass().getClassLoader());
    }
}
package com.hikaru.java.lang;

public class String {
    static {
        System.out.println("这是自定义的String");
    }
}

如上自定义了一个String类,但是在使用的时候并没有使用自定义的(没有输出静态代码块的内容),原因就是系统类加载器在加载String类的时候,会将请求依次委托给上级,直到引导类加载器能够加载String核心类库,所以成功返回,并不会去加载自定义的String类;而下面的加载StringTest类则会请求到引导类加载器,然后再返回给系统类加载器进行加载。

双亲委派机制举例二
image-20221213224722838

JDBC Jar包的加载,其中的SPI接口属于核心类库,会由双亲委派机制委派给引导类加载器加载,而SPI接口的具体实现类会涉及到一些第三方的jar包,显然不属于核心类库,则会反向委派由系统类加载器加载,具体是通过线程上下文加载器ContextClassLoader加载jdbc.jar。

双亲委派机制的优势
  1. 避免了类的重复加载

  2. 保护了程序的安全,防止核心API被随意篡改

    如上面创建的String,可能存在恶意代码;或者在引导类加载器目录下自定义类,会对引导类加载器产生影响(实际上会报错:Prohibited package name

沙箱安全机制

上面自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),这时候运行自定义的String说没有main方法,就是因为实际加载的不是自定义的String,这样可以保证对Java源代码的保护,这就是沙箱安全机制。

6 类的主动使用与被动使用

判断两个class对象为同一个类的两个必要条件:

  1. 类的完整类名必须一致,包括包名
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

也就是说,两个对象来自于同一个class文件,而且被同一个虚拟机加载,但是只要加载的类加载器的实例对象不同,那么这两个类对象也是不同的

Java程序对类的使用方式分为:主动使用和被动使用

主动使用,分为七中情况:

  1. 创建类的实例

  2. 访问某个类或接口的静态变量,或者对该静态变量赋值

  3. 调用类的静态方法

  4. 反射(Class.forName(".."))

  5. 初始化一个类的子类

  6. Java虚拟机启动时被标明为启动类的类

  7. JDK 7开始提供的动态语言支持:

    java.lang.invoke.MethodHandle实例的解析结果

    REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化

除了上面的七种情况,其他使用Java类的方式都被看做是对类的被动使用,被动使用都不会导致类的初始化

7 对类加载器的引用

JVM必须知道一个类型是由启动类加载器加载的,还是由用户类加载器加载的。如果是由用户类加载器加载的,JVM会将这个类加载器的引用作为类型信息的一部分放入到方法区中。所以当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

posted @ 2023-05-17 18:55  Tod4  阅读(55)  评论(0编辑  收藏  举报