类加载和类加载器
本文最后更新于:2024年10月29日 下午
类的生命周期
类的生命周期包括加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中,验证、准备、解析三个阶段统称为连接阶段。
具体的加载顺序为:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。
加载
加载阶段是类加载过程的第一步,主要是完成以下步骤:
- 通过类的全限定名来获取类的二进制数据。
- 将类的二进制数据加载到内存中,并创建一个
java.lang.Class
对象来封装这些数据。 - 将字节流中的静态存储结构转化为方法区的运行时数据结构。
加载这一步主要是依靠类加载器所完成的,具体是哪个类加载器加载由双亲委派模型来决定。类加载器首先会检查这个类是否已经被加载过,如果没有,类加载器会根据类的全限定名来查找类的二进制数据。
类加载器会将类的二进制数据加载到内存中,并创建一个 java.lang.Class
对象来封装这些数据,这个 java.lang.Class
对象就代表了这个类。
验证
验证阶段主要是确保类的字节流符合 JVM 规范,不会危害 JVM 的安全。
验证阶段主要包括四个部分:
- 文件格式验证:验证字节流是否符合 JVM 规范。
- 元数据验证:对类的元数据进行语义分析,以保证不存在不符合 Java 语言规范的元数据。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
- 进行内存分配的对象仅仅包括类变量(static),而不包括实例变量。
- 这个阶段为类变量分配内存并设置默认初始值,这里的初始值是数据类型的零值(0、null、false)。如果是 final 修饰的类变量,那么这个初始值就是在准备阶段设置的。
解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
其实就是根据符号表中内容,将对应字段转换为内存地址。
初始化
初始化阶段是类加载过程的最后一步,主要是执行类构造器 <clinit>()
方法的过程。
类构造器 <clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
- 遇到
new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 - 使用
java.lang.reflect
包的方法对类进行反射调用时。 - 初始化一个类,如果其父类还没有初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()
方法的类),虚拟机会先初始化这个主类。 - 当使用 JDK 1.7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
Redis 过期
类加载器
类加载器是 Java 虚拟机的一项重要功能,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个引用指向加载它的 ClassLoader。
- 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
JVM内置的类加载器
JVM 内置的类加载器主要有以下几种:
- 启动类加载器(Bootstrap ClassLoader):负责加载 Java 的核心类库,如
rt.jar
、resources.jar
、charsets.jar
。 - 扩展类加载器(Extension ClassLoader):负责加载 Java 的扩展类库,如
jre/lib/ext
目录下的 jar 包。 - 应用程序类加载器(Application ClassLoader):负责加载应用程序的类,是最常用的类加载器。
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。
自低向上的类加载器层次结构如下:
1 |
|
双亲委派模型
双亲委派模型是类加载器的一种工作机制,其主要思想是:当一个类加载器收到类加载请求时,它首先会将这个请求委派给父类加载器去完成,只有在父类加载器无法完成这个加载请求时,子类加载器才会尝试自己去加载。使用的设计模式是责任链模式。
graph LR; A[AppClassLoader] -->|委派| B[ExtClassLoader]; B -->|委派| C[BootstrapClassLoader]; C -->|加载类| D[Class];
为什么要使用双亲委派模型
双亲委派模型的主要目的是为了保证 Java 类的安全性,防止用户自定义的类加载器加载 Java 核心类库,从而破坏 Java 的安全性。同时,双亲委派模型还可以避免类的重复加载,提高类的加载效率。
为什么要打破双亲委派模型
有时候,我们需要自定义类加载器来加载一些特殊的类,这时就需要打破双亲委派模型。
如何打破双亲委派模型
打破双亲委派模型的主要方法是重写ClassLoader中的 loadClass()
方法:重写 loadClass()
方法,自定义类加载逻辑。
如果不打破双亲委派模型,那么自定义的类加载器只需要继承 ClassLoader
类,然后重写 findClass()
方法即可。
例如
1 |
|