Java类加载机制
简介
类的加载:
将类的.class文件中的二进制数据读取到内存中,将其放在运行时数据区的方法区中,在堆内存中创建一个java.lang.Class
对象,用来封装在方法区内的数据结构。
Class对象:
封装类在方法区内的数据结构,提供访问方法区的数据结构的接口。
注:不需要等到某个类被“首次使用”再加载,允许预先加载。
加载.class文件的方式:
- 本地系统中直接加载
- zip,jar文件中加载
- Java源文件动态编译
类的生命周期
类加载的过程:
- 加载(loading)
- 连接
- 验证(verification)
- 准备(preparation)
- 解析(resolution)
- 初始化(initialization)
- 使用(using)
- 卸载(unloading)
注:
- 加载,验证,准备和初始化发生的顺序是确定的,而解析阶段则不一定。
- 解析可能发生在初始化之后(运行时绑定/动态绑定/晚期绑定)
- 这些阶段的先后顺序是开始的时机,不代表运行或完成的时机(有可能先开始的后完成)。
加载
查找并加载类的二进制数据。
功能:
- 通过类的全限定名来获取其定义的二进制字节流。
- 将字节流代表的静态存储结构转换为方法区的运行时数据机构。
- 在堆内存中生成一个代表该类的
java.lang.Class
对象,作为该数据的访问入口。
连接
验证
确保被加载的类的正确性。(非必要)
行为:
- 文件格式验证:验证字节流是否符合Class文件格式的规范。
- 元数据验证:对字节码描述的信息进行语义分析。
- 字节码验证:通过数据流和控制流分子,确定程序语义是否合法,符合逻辑。
- 符号引用验证:确保解析动作正确执行。
准备
为类的静态变量分配内存(方法区),并将其初始化为默认值。
注意:
- 内存分配只包括类变量(static)(实例变量:对象实例化随着对象分配在堆内存中).
- 设置的初始值为数据类型默认的零值.
- 若类字段的字段属性为
static final
,则被初始化为所指定的值,而非默认零值. - 对于基本数据类型:类变量(static)和全局变量,若不显式赋值则默认为零值.
- 对于局部变量:使用前必须显式赋值,否则无法通过.
static final
修饰的常量必须在声明时赋值;final
修饰的变量可以声明时赋值,也可以类初始化显式赋值.- 引用类型未显式赋值,则默认为null.
- 数组初始化为赋值,则其中每个元素赋值对应数据类型的零值.
解析
把常量池内的符号引用替换为直接引用的过程.
针对:类或接口,字段,类方法,接口符号,方法类型,方法句柄和调用点限定符.
符号引用:一组符号来描述目标,可以是任何字面量.
直接引用:直接指向目标的指针,相对偏移量或间接定位到目标的句柄.
初始化
为类的静态变量赋予正确的初始值.
设定方式:
- 声明类变量为指定初始值.
- 使用静态代码块为类变量指定初始值.
初始化步骤
- 若类没有被加载和连接,则先加载并连接该类.
- 若类的直接父类没有被初始化,则先初始化其直接父类.
- 若类中有初始化语句,则依次执行初始化语句.
初始化时机
类被主动使用时导致类的初始化.
- 创建类的实例(
new Object()
). - 访问类或接口的静态变量.
- 调用类的静态方法.
- 反射(
Class.forName("io.github.truestoriesavici01.Test")
) - 初始化某个类的子类,其父类也被初始化.
- 虚拟机启动时被表明为启动类的类.
结束
- 执行
System.exit()
方法. - 程序正常结束.
- 程序执行时遇到异常或错误而终止.
- 操作系统导致的虚拟机进程终止.
类加载器
分类
- 启动类加载器(Bootstrap ClassLoader):负责加载\\lib目录或指定的可识别的类库,无法被直接引用.
- 扩展类加载器(Extension ClassLoader):由ExtClassLoader实现,加载\\ext目录或指定的类库,可直接使用.
- 应用程序类加载器(Application ClassLoader):由AppClassLoader实现,负责加载用户类路径(ClassPath)指定的类.
注:三种类加载器相互配合进行加载的.
JVM类加载机制
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖或引用的其他Class也将由这个类加载器负责载入.
- 父类委托:先让父类加载器试图加载该类,若父类加载器无法加载该类时才试图从自己的类路径中加载该类.
- 缓存机制:所有加载过的Class都会被缓存.当程序使用Class时,先从缓存中寻找该Class.若不存在,才读取该类对应的二进制数据,转换为Class对象,存入缓存中.
类的加载方式
- 命令行启动应用时由JVM初始化加载.
- 通过
Class.forName()
方法动态加载. - 通过
ClassLoader.loadClass()
方法动态加载.
Class.forName()
和ClassLoader.loadClass()
的区别
Class.forName()
:将类的.class文件加载到JVM中,会类进行解释,执行类中的static块.ClassLoader.loadClass()
:将.class文件加载到JVM中.
双亲委派模型
简介
当一个类加载器收到类加载请求时,不会自己加载该类,而是委托父加载器.依次向上发出请求,直到传递到顶层的启动类加载器.只有当父加载器没有找到所需的类,无法完成加载时,子加载器才试图自己加载该类.
流程
- 当AppClassLoader加载一个class时,将类加载请求委派给父类加载器ExtClassLoader去完成.
- 当ExtClassLoader加载一个class时,先将类加载请求委托给BootStrapClassLoader去完成.
- 若BootStrapClassLoader加载失败,会使用ExtClassLoader尝试加载.
- 若ExtClassLoader也加载失败,则使用AppClassLoader加载,若加载失败,会报出异常
ClassNotFountException
.
优势
- 防止内存中出现多份同样的字节码.
- 保证Java程序安全稳定运行.
自定义类加载器
- 继承自ClassLoader类.
- 重写
findClass
方法.
参考: