JVM系列之类加载子系统

1、类加载器与类的加载过程

在这里插入图片描述

1.1、类加载器

1.1.1、类加载器子系统的作用

在这里插入图片描述

  • 负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识,魔术,CA FA BA BE
  • Classloader只负责class文件的加载,至于是否可运行,则由执行引擎决定
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射),常量池运行时加载到内存中,即运行时常量池
1.1.2、类加载ClassLoader角色

在这里插入图片描述
将.class文件加载到内存中,经过一系列的操作(加载、链接、初始化),在方法区生成类的模板信息,作为方法区这个类的各种数据访问接口

1.1.3、命名空间
1.1.3.1、怎么判断类的唯一性?
  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性
  • 每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义
  • 否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等
1.1.3.2、命名空间的规则
  • 每个类加载器都有自己的命名空间,命名空间由该加载器所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
  • 在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本
1.1.4、三个基本特征
1.1.4.1、双亲委派模型

双亲委派模型:是指当类加载器接收到加载一个类的请求时,自己并不会去加载这个类,而是委托给父类加载器进行加载,如果父类加载器仍然存在父类加载器,则递归向上传递,直至顶层的启动类加载器,如果父类加载器成功加载该类,则成功返回,如果父类加载无法加载该类,则由子类尝试加载,事实上是一种任务委派的模型。

1.1.4.2、可见性

可见性:是指子类加载器可以访问父加载器加载的类型,但是反过来是不允许的, 不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑

1.1.4.3、单一性

单一性:是指由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载,但是注意,类加载器"邻居"间, 同一类型仍然可以被加载多次,因为相互并不可见

1.1.5、类加载器分类

主要分为引导类加载器和自定义加载器,概念上,将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
在这里插入图片描述

1.1.5.1、获取类加载器

在这里插入图片描述
Java的核心类库,使用引导类加载器进行加载,对于用户自定义类,默认使用系统类加载器进行加载

1.1.5.2、启动类加载器
  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    并不继承自ava.lang.ClassLoader,没有父加载器。
    在这里插入图片描述
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
1.1.5.3、扩展类加载器
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类,父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
    在这里插入图片描述
1.1.5.4、应用程序类加载器(系统类加载器)
  • Java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类,父类加载器为扩展类加载器
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库,该类加载器是程序中默认的类加载器。一般来说Java应用的类都是由它来完成加载
  • 通过ClassLoader.getSystemClassLoader()方法可以后去修改类加载器
1.1.5.5、用户自定义类加载器
1.1.5.5.1、为什么要用自定义类加载器
  • 隔离加载类:例如中间件的Jar包与应用程序Jar包不冲突
  • 修改类加载的方式:启动类加载器必须使用,其他可以根据需要自定义加载
  • 扩展加载源
  • 防止源码泄露:对字节码进行加密,自定义类加载器实现解密
  • 可以实现代码的热部署
1.1.5.5.2、实现步骤
  • 继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器
  • jdk1.2之前,继承并重写loadClass()方法,1.2之后,建议把自定义的类加载逻辑写在findClass()方法中
  • 如果没有太过复杂的需求,可以直接继承URLClassLoader类,可以避免自己编写findClass()方法,及其获取字节码流的方式,使自定义类加载器编写更加简洁
1.1.5.6、数组的类加载器
  • 数组类的Class对象,不是由类加载器去创建的,而是在运行期JVM根据需要自动创建的
  • 对于元素为基本数据类型的数组,没有类加载器
  • 对于元素为引用数据类型的数组,类加载器就是对应的加载该类的类加载器

在这里插入图片描述
在这里插入图片描述
为什么String[]数组的ClassLoader为null呢?因为String.class由启动类加载器加载,在堆内存中没有实例对象

1.1.6、双亲委派模型

双亲委派模型:是指当类加载器接收到加载一个类的请求时,自己并不会去加载这个类,而是委托给父类加载器进行加载,如果父类加载器仍然存在父类加载器,则递归向上传递,直至顶层的启动类加载器,如果父类加载器成功加载该类,则成功返回,如果父类加载无法加载该类,则由子类尝试加载,事实上是一种任务委派的模型。

1.1.6.1、原理

Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
在这里插入图片描述

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
  3. 如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
1.1.6.2、优势
  • 避免类的重复加载
  • 保护程序安全,防止核心API被篡改
1.1.6.3、沙箱安全机制

保证对Java核心源代码的保护,防止核心API被篡改

JVM必须知道一个类型是由启动类加载器加载的,还是由用户类加载器加载的。如果是用户类加载器加载的,JVM会将这个类加载器的一个引用作为类型信息的一部分,保存到方法区中

1.1.7、ClassLoader类源码分析

关于ClassLoader类,是一个抽象类,除了启动类加载器,其他类加载器都继承自他
在这里插入图片描述

loadClass()方法源码分析
在这里插入图片描述

1.1.8、自定义类加载器以及使用场景
1.1.8.1、作用
  • 隔离加载类:例如中间件的Jar包与应用程序Jar包不冲突,在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境
  • 修改类加载的方式:类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点按需进行动态加载
  • 扩展加载源:比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄露:对字节码进行加密,自定义类加载器实现解密,Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码
1.1.8.2、常见场景
  • Tomcat这类Web容器同时部署多个war包,服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。同时多个war之间的类也应该隔离,为了实现类隔离的目标,Tomcat制定了如下规则

    1. 放置在/common目录中。类库可被Tomcat和所有的Web应用程序共同使用
    2. 放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见
    3. 放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见
    4. 放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序都不可见
    5. 为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器, 这些类加载器按照经典的双亲委派模型来实现,如下所示
      在这里插入图片描述
    6. 实现方式为:Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类,重写loadClass()方法或者重写findClass()方法
  • 。。。。。。

1.2、类加载过程

类加载器的加载过程分为以下三个步骤:

  • 加载(Loading):查找并加载类的二进制数据。这个过程由类加载器完成,其实就是从文件、网络等地方读取类的字节码数据,然后将其转换成内部数据结构,并存储在 JVM 的方法区中。
  • 链接(Linking):将类的二进制数据合并到 JVM 的运行时状态中。链接分为三个阶段:
    • 验证(Verification):确保类的字节码符合 JVM 规范,并且不会损害 JVM 的内部状态。
    • 准备(Preparation):为类的静态变量分配内存,并设置默认值。
    • 解析(Resolution):将类的符号引用转换为直接引用。符号引用指的是类、方法、字段的符号名称,而直接引用是指内存中实际的地址。解析的目的是为了支持动态绑定,即在运行时确定方法、字段的具体实现。
  • 初始化(Initialization):执行类的初始化代码,即静态初始化器和静态变量赋值语句。初始化的时机有两种情况:一是在实例化对象时,如果该类还没有被初始化,则需要先触发其初始化;二是在访问类的静态变量或静态方法时,如果该类还没有被初始化,则需要先触发其初始化。

总的来说,类加载器负责将类的二进制数据加载到 JVM 中,而链接和初始化的过程则是将类的二进制数据转换为可执行的代码,并存储在 JVM 中,从而使得应用程序可以执行该类的方法。在加载过程中,由于可以使用自定义类加载器,因此可以在加载过程中进行一些定制化的操作,例如实现类的加密、动态代理等。

在这里插入图片描述
类加载的流程图
在这里插入图片描述

1.2.1、加载

加载是类加载过程的一个阶段,并不是说类的加载。

加载:就是将字节码文件加载到内存中,并且在内存中构建出Java类的原型–类模板对象。

类模板对象(instanceKlass),其实就是Java类在JVM内存中的一个快照,Java的对象并没有映射成C++的原生对象,而是使用了OOP-KLASS模型来表示Java对象,JVM将从字节码文件中解析出的常量池、 类字段、类方法等信息存储到模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射

1.2.1.1、加载过程分为三个步骤:
  • 通过一个类的全限定名获取定义此类的二进制字节流。

    1. 从本地系统中直接加载
    2. 通过网络获取,典型场景:Web Applet
    3. 从zip压缩包中读取,成为日后jar,war格式的基础
    4. 运行时计算生成,使用最多的是:动态代理技术
    5. 由其他文件生成,典型场景:JSP应用
    6. 从专有数据库中提取.class文件,比较少见
    7. 从加密文件中获取,典型的防Class文件被反编译的保护措施
  • 将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构。

  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

1.2.1.2、类模型与Class实例的位置
  • 类模型的位置:加载的类由JVM创建相应的类结构,类结构会存储在方法区(JDK 1.8之前:永久代;JDK1.8之后:元空间)

  • Class实例的位置:JVM将.class文件加载到方法区后,会在堆内存中创建一个java.lang.Class类的对象实例,用来封装类位于方法区内的数据结构。该Class对象是在加载类的过程中创建的,每个类都对应一个Class对象

在这里插入图片描述
Class类的构造方法是私有的,只有JVM能够创建。java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据和入口。通过Class类提供的接口,可以获得目标类所关联的.class文件具体的数据结构:方法、字段

1.2.2、链接

链接过程又分为3个阶段 验证(Verification)、准备(Preparation)、解析(Resolution)

1.2.2.1、验证(Verification)

验证(Verification)的目的是确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
在这里插入图片描述

  • 文件格式验证
    1. 是否以0xCAFEBABE开头(魔数,Java虚拟机识别)
    2. 主次版本号(Minor Version、Major Version)
    3. 常量池的常量是否有不被支持的常量类型
    4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  • 语义验证(需要符合Java语法)
    1. 类是否有父类,除了Object类之外,所有的类都应该有父类
    2. 类的父类是否继承了不允许被继承的类(被final修饰的类)
    3. 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
    4. 类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同
  • 字节码验证
    1. 通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的
    2. 对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为
    3. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况
    4. 保障任何跳转指令都不会跳转到方法体之外的字节码指令上
  • 符号引用验证
    1. 通过字符串描述的全限定名是否能找到对应的类
    2. 符号引用中的类、字段、方法的可访问性是否可被当前类访问
1.2.2.2、准备(Preparation)
  1. 类变量(static修饰的成员变量)分配内存,并且设置该类变量的初始值,即零值,下表中是各种数据类型(基本数据类型和引用数据类型)的零值。不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
    在这里插入图片描述
  2. 不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到Java堆中
  3. 在这个阶段不会像初始化阶段那样会有初始化或者代码被执行。
1.2.2.3、解析(Resolution)
  • 将类、接口、方法、字段的符号引用转换为直接引用,
    1. 符号引用就是一组符号来描述引用的目标。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
    2. 直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info

举例分析符号引用和直接引用:符号引用就是一些字面量的引用,和虚拟机内部数据结构和内存分布无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的的,比如println()方法被调用时,系统需要知道该方法的位置,以及对应的方法体,举例:输出操作System.out.println()对应的字节码:invokevirtual # 24 <java/io/PrintStream.println>
在这里插入图片描述
以方法为例,Java虚拟机为每个类都准备一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,需要知道这个方法在方法表中的偏移量就可以直接调用该方法,通过解析操作,符号引用就可以转变为目标方法在方法表中的位置,从而使得方法被成功调用。

事实上,在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但是解析操作往往会伴随着JVM在执行完初始化之后再执行

1.2.3、初始化

初始化阶段是类加载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时类才会执行Java字节码(到了初始化阶段,才真正开始执行类中定义的Java代码)

  • 初始化阶段是执行类构造器方法()的过程,该方法只会被执行一次,且虚拟机的实现需要保证多线程情况下被正确地同步枷锁,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,如果没有类变量和静态代码块,也不会有()方法
  • 构造器方法中指令按照语句在源文中出现的顺序执行
  • ()不是类的构造器(关联:()是类的构造器)
  • 若该类具有父类,JVM需要保证子类的()执行前,父类的()已经执行完毕
  • Java编译器并不会为所有的类都产生()方法。哪些情况下不会生成()方法
    1. 一个类中没有任何类变量以及静态代码块
    2. 一个类声明了类变量,但是没有显式赋值
    3. 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
      在这里插入图片描述
      在这里插入图片描述
1.2.4、类的使用(Using)
  • 使用new关键字创建对象
  • 使用反射的方式创建对象
  • 调用clone()方法创建对象
  • 使用反序列化方式得到对象

类的主动使用和被动使用(类都会被加载到方法区中,区别在于是否执行类的<clinit>()方法),如何判断呢?
如果类变量需要运行才能确定值(执行代码),那么肯定是主动使用,会执行()方法,除此之外都是被动使用
-XX:+TraceClassLoading。该参数可以打印出JVM虚拟机加载的类

Java虚拟机规范严格规定了,有且仅有六种情况,必须立即对类进行初始化(主动使用),除此之外都是被动使用

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
    1. 使用new关键字实例化对象
    2. 读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
    3. 调用一个类型的静态方法的时候
  • 对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化
  • 初始化类的时候,发现父类没有初始化,则先触发父类初始化
  • 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
  • 只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化
  • 当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化
    在这里插入图片描述
    在这里插入图片描述

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

1.2.5、类的卸载(Unloading)
1.2.5.1、类、类的加载器、类的实例之间的引用关系
  1. 在类加载器内部实现中,用一个Java集合(Vector)存放所加载类的引用
    在这里插入图片描述
  2. 一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法就可以获得它的类加载器,由此可见某个类的Class实例与其加载的类加载器之间为双向关联关系
  3. 一个类的对象实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回对象所属类的Class对象的引用
1.2.5.2、类的生命周期

当Sample类被加载、链接、初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可达时,Class对象的生命周期就结束了,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。

在这里插入图片描述

  1. 事实上Sample对象也指向Sample类的二进制数据结构,在对象头中有一个指向方法区类元数据的指针
  2. loader1变量和obj变量间接引用Sample类的Class对象,而objClass变量则直接引用Sample类的Class对象。
  3. 如果在运行中,将上图左侧三个引用类型变量都置为null,此时Sample对象生命周期结束,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据也被卸载。
  4. 当再次需要使用时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载。
  5. 如果不存在Sample类会被重新加载,在Java虚拟机的堆内存中生成一个新的代表Sample类的Class实例。
1.2.5.3、什么时候进行类的卸载?
  • (直接和间接)引用Sample类的Class对象的变量为null,由Sample类的Class对象创建的类加载器对象实例对象的生命周期都结束,满足以上三个条件后,并不是和对象一样立即被回收,而是仅仅允许回收
  • 被启动类加载器的类型在整个运行期间是不可能被卸载的(JVM和JSL规范)
  • 被系统类加载器和扩展类加载器加载的类型在运行期不太可能被卸载,因为系统类加载器或者扩展类的实例基本上在整个运行期间总能直接或者间接访问到
  • 被开发者自定义的类加载器加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于调用虚拟机的垃圾收集功能才可以做到
  • 总和以上三点,一个已经加载的类型被卸载的几率很小,几乎不会被卸载
1.2.5.4、方法区的垃圾回收
  • 方法区的垃圾收集主要是常量池中废弃的常量和不再使用的类型
  • 只要常量池中的常量没有被任何地方引用,就可以被回收
  • 判断一个类(CLASS)能否被回收,需要同时满足满足三个条件,满足以上三个条件,仅仅是允许被回收,并不是和对象一样,没有引用了就必然被回收
    1. 该类的所有实例都已经被回收,Java堆内存中不存在该类以及派生子类的实例
    2. 加载该类的类加载器已经被回收
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

加载、验证、准备、初始化、使用和卸载这六个阶段的顺序是确定的,解析阶段不一定,在某些情况下可以在初始化阶段之后再开始,为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定,其实就是多态),例如子类重写父类方法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值