JVM - 的类加载器(类加载子系统)

2023-11-02

类加载子系统(Class Loader)作用

类加载子系统就是负责从文件系统或者网络中加载class文件。类加载子系统(Class Loader)只负责Class文件的加载。
至于这个文件是否能够正常运行,它不负责管理。是由执行引擎(Execution Engine)来决定的。
而加载的类信息存放于一块称为方法区(元空间,Method Area)的内存来管理的。
在这里插入图片描述

  1. class文件就存放在硬盘上,可以理解为设计师画在纸上的图纸(模板),而最终的这个模板在执行的时候是要加载到JVM当中去。根据这个模板实例出我们需要的n个一模一样的实例(对象)。
  2. class文件在加载到JVM中的时候,就称为DNA元数据模板,放在方法区当中。
  3. 在class文件到JVM成为元数据模块的这个过程中,是个过程那么它就必须要有一个运输工具。而类加载系统(Class Loader)就扮演着这么一个角色。一个快递员的角色。

类加载过程

类加载的过程(图解)
在这里插入图片描述

可以说:类加载的过程就是包括了加载、验证、准备、解析、初始化的五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则又不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。

在这里也要注意到这几个阶段是按顺序开始,而不是按顺序进行或完成。 因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中会调用或激活另一个阶段。

加载阶段(Loding)

  1. 通过类名(地址)获取此类的二进制字节流。(就是根据类的地址,存硬盘上读取类的信息)
  2. 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口。(将信息读入到方法区,生成Class类的对象)

加载.class文件的方式(了解):
● 从本地系统中直接加载。
● 通过网络获取,典型场景:Web Applet。
● 从zip压缩包中读取,成为日后jar、war格式的基础。
● 运行时计算生成,使用最多的是:动态代理技术。
● 由其他文件生成,典型场景:JSP应用。
● 从专有数据库中提取.class文件。
● 从加密文件中获取,典型的防止Class文件被反编译的保护措施。

连接阶段(Linking)

链接(Linking)阶段分为3个步骤:验证、准备、解析。

验证(Verification)

检验被加载的类是否具有正确的内部结构,并和其他类协调一致。

也是为了确保被加载类的Class文件的二进制字节流中包含的信息符合当前虚拟机的要求。保证被加载类的正确性,不会危害虚拟机自身安全。

主要有四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 验证文件格式是否一致:
    class文件在文件开头具有特定的文件标识(字节码文件都以0xCA FE BA BE标识开头的)咖啡baby这可能就是java标志是个茶杯的样子了吧,哈哈。

  2. 元数据验证:
    对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,例如这个类是否具有父类;是否继承浏览器不允许被继承的类(final修饰的类). . .

  3. 字节码校验(验证):
    Java虚拟机对字节流进行数据分析,这些字节流代表的就是类的方法。
    在验证中会进行大量的检查,比如:
    ● 保证局部变量在赋予合适的值以前不能被访问
    ● 类中的字段中必须总是被赋予正确类型的值
    ● 类的方法被调用时总是传递正确的数值和类型的参数。

  4. 符号引用验证
    就是方法之间的互相调用。
    如果包含在class文件中的符号引用被解析时,class文件检验器将进行第四趟检查。大多数虚拟机都采用延迟装载类的策略,只有类被真正调用的时候才会解析。所以本次验证会再第三趟扫描之后很长一段时间,字节码被执行时才进行。

准备(Preparation)

● 准备阶段则负责为类的静态属性分配内存,并设置默认初始值 (比如int的默认初始值就是0。)
● 不包含final修饰的static修饰的常量,它是在编译时已经进行初始化赋值了
● 也不会为实例分配初始值。对象在这个阶段是还没有创建的,在方法区里进行具体的操作,最后都是在堆内存当中。

举例:

pubuic static int demo = 10;
在准备阶段的时候,demo的初始值为0,而不是10。在编译器才具体的赋值
0才是demo的默认初始值

解析(Resolution)

将类的二进制数据中的符号引用替换成直接引用
(符号引用就是Class文件的逻辑符号,直接引用指向的方法区中某一地址)

说人话就是:将字节码中符号引用替换成直接引用

举例:
编写代码 方法1 中调用 方法2 (这就可以认为是一个符号引用)
一旦类加载到内存后把符号的引用地址换成 内存的地址引用

初始化(Initialization)(重点)

初始化,就是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化赋值。(真正的给值)

类什么时候初始化

  1. 创建类的实例,也就是new一个对象的时候。
  2. 访问某个类或接口的静态变量,或者对静态变量赋值的时候。
  3. 调用类的静态方法的时候。
  4. 通过反射的时候(Class.forName(“类的地址”) )。
  5. 初始化一个类的子类的时候(会首先初始化这个子类的父类)

类的初始化顺序

对 static 修饰的变量或语句块进行赋值。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

大致顺序:

  1. 先初始化静态的,多个静态的按照从上向下的顺序执行(从父类向下进行。在一个类中的话,存上到下的顺序进行)
  2. 如果类有父类,则先初始化父类的静态,然后是子类。
  3. 如果是创建对象,先调用父类的构造方法,然后是子类自己的构造方法。

完整顺序流程就是:
父类的static --> 子类的static --> 父类的构造方法(当调用这个子类的构造方法的时候,才是会被加载到的) --> 子类的构造方法

代码的体现一下(因为是jvm底层的操作,在代码上也没有太好的体现出来)

public class Demo {
    static int num = 10; //准备阶段为静态的变量进行初始化,赋值(赋默认值为0)初始化阶段再赋值为10
    static {
        num = 100;
    }
    
    public static void main(String[] args) {
        System.out.println(num);
    }
    //num的值是: 加载时为0  到达初始化。因为是static修饰的,从上到下,就先变为10,再到最后的100
}

类加载器的分类

在这里插入图片描述

● 站在JVM的角度来看,类加载器是可以分为两种的:

  1. 引导类加载器(启动类加载器 Bootstrap ClassLoader)不是由java语言来写的,由C/C++来写的。
  2. 其他所有类加载器。这些类加载器都由java语言来实现,独立的存在于虚拟接的外部,并且它们都全部继承自抽象类 java.lang.ClassLoader的这个类中。

● 站在java开发者的角度来看,类加载器就应该划分的更加细致一点,自jdk1.2之后java就一直保持着三层类加载器。
在这里插入图片描述

引导类加载器(启动类加载器 BootStrap ClassLoader)

这个类加载器使用的是C/C++语言来实现的,是嵌套在JVM的内部。它是用来加载 java的核心类库。

它并不继承于java.lang.ClassLoader 是没有父加载器的。

它是负责加载扩展类加载器 和 引用类加载器(系统类加载器)。并为它们指明父类的加载器。

出去安全考虑 ,引导类加载器只加载存放在<JAVA_HOME >\lib目录里面的或者被-Xbootclasspath 参数锁指定的路径中存储的类。

扩展类加载器(Extension ClassLoader)

是由Java语言来编写的,由sun.misc.Launcher$ExtClassLoader来实现的。

它派生于ClassLoader的这个类。
所有Java方面的类加载器都是需要继承ClassLoader这个类的
在这里插入图片描述

它就负责加载从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动又扩展类加载器加载

在这里插入图片描述

应用程序类加载器(系统类加载器 Application ClassLoader)

由java语言编写的,由sun.misc.Launcher$AppClassLoader来实现的

它派生于ClassLoader的这个类。

加载我们自己定义的类,用于加载用户类路径(classpath)上的所有类。
负责加载用户类。

这个类加载器是程序中默认的类加载器。

用户自定义类加载器

例如Tomcat,在Tomcat之中就有着一些内置的用户级别的类加载器。

在Java中。ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(但不包括启动类加载器)

package com.wang.javaforword.jvm.classloader;

public class ClassLoadDemo {
    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);
        //理应为引导类加载器(启动类加载器)但是这个加载器由C写的,java无法获得,所以结果为null

        //自己写的类是由应用程序类加载器加载的
        ClassLoader classLoader = ClassLoadDemo.class.getClassLoader();
        System.out.println(classLoader);

        //获取到自己写的类的上一级是什么类型的类加载器
        System.out.println(classLoader.getParent());
    }
}

在这里插入图片描述

双亲委派机制

Java虚拟机对Class文件采用的是按需加载的方式,也就是说当需要该类时才会将它的Class文件加载到内存中生成Class的对象。

而且加载某个类的Class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类来处理,这就是一个任务委派模式。

在这里插入图片描述

除引导类加载器外,所有其它类加载器都有其父类加载器。

工作原理:

  1. 如果一个类加载器收到了类加载的请求,它并不会自己先去加载这个类,而是会把这个请求委托给父类的加载器去执行。
  2. 如果父类的加载器还存在其父类的加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器(也就是引导类记载器)。
  3. 如果父类加载器可以完成类的加载任务,就成功返回。倘若父类的加载器没有或者无法完成记载的这个任务,子加载器才会依次尝试自己去加载。这就是双亲委派机制

如果均加载失败的话,就会抛出ClassNotFoundException(类找不到异常)

小问题:
我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们 new String()时,会将加载创建核心类库中的 String 对象还是创建我们自己创建 的 String 类对象?
在这里插入图片描述

在这里它因为双亲委派的机制,内加载器会向父类的方向走,通过java.lang.String最终加载的是java核心库中的String对象。

为什么采用双亲委派机制?

使得Java类随着类加载器不同而具备带优先级的层次关系,如java.lang.Object(位于rt.jar内),无论那个类加载器要加载该类,最终都委派给顶层引导类加载器,因此Object类在程序的各种类加载环境中都是同一个类。

如果没有双亲委派,用户自定义重名的类,将会使得系统带有多个同名的类,使得基础的Java类型体系混乱

双亲委派优点?

  1. 它是安全,可避免用户自己编写的类被动态替换 Java 的核心类,如 java.lang.String
  2. 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加 载)

双亲委派模型对于保证Java程序的稳定运行十分重要,它实现却很简单
首先就是检查是否被加载过,若没有加载则调用父类加载器的loadClass方法,若父类加载器为空,则默认使用引导类加载器作为父类加载器,如果加载还是失败,则调用自身的findClass()方法来加载。

破坏双亲委派情形:使用JNDI服务、代码模块热部署

类的主动使用/被动使用

JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就会有被动使用。被动使用是不会初始化的。

主动使用

  1. 通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,它肯定会导致类的加载并且初始化。

  2. 访问某个类或接口的静态变量,或对该静态变量赋值。(包括读取和更新)

  3. 访问类的静态方法。

  4. 反射,通过反射的方式获得类的对象,也是对类的主动使用

  5. 初始化子类会导致父类的初始化。

  6. 执行该类的main函数的时候,也就是Java虚拟机启动的时候被标记为启动类的类。

  7. JDK1.7开始提供动态语言支持(如我们可以在JVM上使用脚本语言的引擎调用JavaScript(动态语言)代码):
    java.lang.invoke.MethodHandle实例的解析结果REF_getStastic、REF_putStastic、REF_invokeStastic句柄对应的类没有初始化,则进行初始化。

以上这里都是属于类的主动使用

被动使用

其实除了以上的几种主动使用,其余的都算做事被动使用。

引用该类的静态常量,注意是常量。是不会导致初始化的。但是也是存在意外的,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得到结果的常量就会导致初始化。

比如:

public final static int NUMBER = 10;
//这个就不会初始化,它属于被动使用

在这里插入图片描述

public final static int RANDOM = new Random().nextInt() ;
//因为通过了一定的计算,所以会导致类的初始化,为主动使用

在这里插入图片描述

构造某个类的数组时不会导致该类的初始化。

比如:

Student[] students = new Student[10];

在这里插入图片描述

主动使用和被动使用的区别就在于类调用的时候是否进行了初始化。

总篇:-----> JVM概述
二、JVM - 运行时数据区
三、JVM - 本地方法接口
四、JVM - 执行引擎

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JVM - 的类加载器(类加载子系统) 的相关文章