JVM(三)类生命周期和类加载机制
类的生命周期
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 字节码信息保存到内存方法区,在方法区生成一个InstanceKlass对象,保存类的所有信息。
- 在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息
类加载器:加载阶段是开发人员可控性最强的阶段,类加载器可以是系统系统的,也可以是自定义的。
加载方式:类的二进制字节流并没有限定说必须从Class文件获取,其他获取的渠道举例:
- 从本地文件系统加载
- 从数据库中获取
- 从网络下载等
验证
验证是连接阶段的第一步,验证的主要目的就是按照虚拟机的要求去检查Class字节流,确保这个字节流是符合要求的,不会有安全性问题等。
准备
准备阶段主要是为类的静态变量(Static变量)分配内存,并将其初始化为默认值(0,0L,,null,false等这种)。
注意点:
- 准备阶段只给类变量分配内存,不会给实例变量分配内存。
- 准备阶段正常只会赋零值。
public static int value 123;
准备阶段后,value=0 - 加了final,会直接赋初始值,
public static final int value 123;
准备阶段后,value=123
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。包括(类或接口解析;字段解析;方法解析;接口方法解析)
符号引用:符号引用以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
初始化
初始化是为类的静态变量赋予正确的初始值(前面准备阶段是为变量赋零值),JVM负责对类进行初始化,主要对类变量进行初始化。
初始化阶段就是执行类构造方法中的<c1init>
方法,该方法是Javac自动生成的。
在Java中对类变量进行初始值设定有两种方式:
- 声明类变量
public static int value 123;
静态代码块
public static int value; static{ value = 123; }
初始化步骤
- 如果这个类还没有被加载和连接,则程序先加载并连接该类
- 如果该类的直接父类还没有被初始化,则先初始化其直接父类
- 如果类中有初始化语句,则系统依次执行这些初始化语句
加载连接--初始化父类--依次初始化
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 调用类的静态方法
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 初始化某个类的子类,则其父类也会被初始化
- 创建类的实例,也就是new的方式
- 反射(如Class.forName("com.pdai.jvm.Test)
- 运行main方法
卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由JVM 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器和类加载机制
类加载是什么?
- Class文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、准备、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程称为虚拟机的类加载机制。
- 与编译时需要连接的语言不同,Java中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
- 一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备,解析三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持Java的动态绑定。
类加载过程
加载,验证,准备,解析,初始化
有哪些类加载器?
启动类加载器 在JVM启动时创建,负责加载最核心的类(
%JAVA_HOME%/lib
目录下的jar包和类),例如Object、System等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用null代替即可,因为启动类加载器通常由操作系统实现,并不存在于JVM体系。扩展类加载器
从JDK9开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类(
%JRE_HOME%/lib/ext
目录下的 jar 包和类),比如XML、加密、压缩相关的功能类等。应用类加载器
也称系统类加载器,负责加载用户类路径上的类库(当前应用
classpath
下的所有 jar 包和类),可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器
通过继承ClassLoader并重写
findClass
方法实现。如果我们不想打破双亲委派模型,就重写
ClassLoader
类中的findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写loadClass()
方法。
双亲委派模型是什么?
双亲委派模型的执行流程:
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。
(自底向上查找判断类是否被加载,自顶向下尝试加载类)
类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能(每个类加载器内部都持有一个父类加载器的引用)。
双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。
如何判断两个类是否相等?
类名和类加载器都相同
JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。
只有两者都相同的情况,才认为两个类是相同的。
即使两个类来源于同一个 Class
文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
双亲委派模型的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
比如我们编写一个称为 java.lang.Object
类的话,双亲委派模型可以保证加载的是 JRE 里的那个 Object
类,而不是你写的 Object
类
打破双亲委派模型方法
- 重写
loadClass()
方法
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()
方法来加载类)。重写 loadClass()
方法之后,我们就可以改变传统双亲委派模型的执行流程
例如:我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader
来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
- 线程上下文加载器
线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用
场景:高层的类加载器需要加载低层的加载器才能加载的类(逻辑打破,应用类加载器自己加载时还是向上找,向下加载)
SPI 中,SPI 的接口(如 java.sql.Driver
)是由 Java 核心库提供的,由BootstrapClassLoader
加载。
而 SPI 的实现(如com.mysql.cj.jdbc.Driver
)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。
默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader
)也会用来加载 SPI 的实现。按照双亲委派模型,BootstrapClassLoader
是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。