JVM类加载机制
类加载系统
JVM
核心的结构里面有一个就是 类加载系统。 一个java
代码经过编译器后变成class字节码文件
,后续在运行时,字节码需要通过JVM
的类加载系统执行运行。
类加载系统
- 类的加载过程
- 类的生命周期
- 类加载器种类
- 类加载机制
类生命周期
类的生命周期包括: 加载、链接(验证、准备、解析)、初始化、使用和卸载,
加载
- 通过类全限定名来获取二进制字节流、
- 字节流描述的静态存储结构转换为
方法区
的运行时数据结构 - 在
Heap
中生成一个代表这个类的Class
对象,作为方法区访问这些数据的入口
链接
验证 确保被加载的类的正确性
- 文件格式验证,字节流是否符合
Class
文件格式,如0xCAFFBABE
开头、版本号、常量类型等等 - 元数据验证, 多字节码描述性信息进行语义分析、确保符合规范
- 字节码验证
- 符号引用验证
- 文件格式验证,字节流是否符合
准备 为类的 静态变量 分配内存,并将其初始化成默认值
解析 把类中的符号引用转换为直接引用(直接指向目标的指针、相对偏移量或间接定位到目标的句柄), 主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等符号引用进行。
类加载过程
加载、链接(验证、准备、解析)、初始化 属于 类加载的过程
类加载过程从上图可看见
- 第一个阶段
Loading
通过类的全限定名获取到该类的
class
二进制字节码流数据,将二进制流数据代表的静态存储结构转换成方法区运行时的数据结构
- 第二个阶段
Linking
- 验证
Verify
: 保证class
文件字节流是符合JVM
的要求,确保安全 - 准备
Prepare
: 为静态字段分配内存并设置初始化默认值(被final
修饰的static
字段不会设置,在编译阶段就分配了) - 解析
Resolve
: 解析是为了将常量迟的符号引用转换为直接引用(实际引用),如果符号引用指向未被加载的类,或未被加载的类的字段或方法,那么解析触发了这个类的加载(但是不一定触发这个类的Linking
和Initialization
)
- 验证
- 第三个阶段
Initialization
初始化阶段是执行类的构造方法,
init()
的过程,(编译器自动收集类的所有变量赋值动作和静态代码块语句合并的) 若该类有父类,JVM
保证父类先执行init
然后执行子类的init
类加载器的分类
上面描述了类加载的机制,那么用于类加载的组件我们叫类加载器,而且类加载器是有分类的
Bootstrap ClassLoader
启动类加载器,是
JVM
内部实现的,Java
语言程序无法直接操作,主要用来加载Java
核心类库,诸如rt.jar
、resources.jar
、sun.boot.class.path
目录下的包,用于JVM
运行所需的包(一般只加载 包名为java
、javax
、sun
开头的类)它加载
Extension ClassLoader
和Application ClassLoader
,并成为它们的父类加载器
Extension ClassLoader
扩展类加载器(
sun.misc.Launcher$ExtClassLoader
实现), 派生继承java.lang.ClassLoader
, 父类加载器是 启动类加载器, 加载lib/ext
目录下的加载类库。可以将自己的包放到该目录下,就会自动加载进入
Application ClassLoader
应用程序类加载器(
sun.misc.Launcher$AppClassLoader
实现), 派生继承java.lang.ClassLoader
, 父类加载器是 启动类加载器,加载classpath
或系统java.class.path
路径洗的类库程序的默认的类加载器,
Java
程序中的类,由它加载完成
- … (自定义类加载器)
在开发过程中,可能需要使用到一些操作,可以自定义类加载器,达到一些方式,比如应用程序的启动自动激活检查等等,或者密钥的验证等等。 可以开发自定义的网络加载
Java
类, 并进行加解密操作实现自定义类加载器的过程
- 继承
java.lang.ClassLoader
,重写findClass()
方法- 也可以继承
URLClassLoader
类,重写loadClass()
方法(可以参考AppClassLoader
或ExtClassLoader
)
类加载机制
JVM
对 class文件 采用的是 按需加载 方式,只有需要使用到这个类的时候,JVM才会将其 class文件加载到内存中并产生 class对象
采用的机制: 双亲委派机制,即把加载请求交给父类处理的一直任务模式
工作方式:
- 一个 类加载器 接收到 类加载 请求,会先将这个请求委托给 父类加载器 执行,
- 父类加载器 还存在 父类加载器,则一直向上委托,直到
Bootstrap ClassLoader
- 父类加载器 完成加载任务,则直接返回成功结果,否则的话,则交由 子类 尝试加载,如果子类也加载失败,则抛出
ClassNotFoundException
类加载的方式
- 命令行应用启动 交给
JVM
初始化加载 - 通过
Class.forName()
来完成动态加载 - 通过
ClassLoader.loadClass()
动态加载
其他加载类的方式
在 JVM
体系中,并不是所有的类加载都是 使用的 双亲委派机制 加载的,也有一些是使用的是 SPI
(Service Provider Interface
) 服务提供者接口实现的。
定义 SPI
,是为了允许接口的实现由第三方提供,诸如 JDBC
、JNDI
等等,这些 SPI
的接口都是属于 Java
核心库,一般在 rt.jar
中,由 Bootstrap ClassLoader
加载,
但是 Bootstrap ClassLoader
是由第三方提供实现的,所以它无法直接加载这些实现类,且同时存在 双亲委派机制 的迷失,Bootstrap ClassLoader
也无法反向委托给其他 ClassLoader
来加载 SPI
的实现类
这样的,就需要一种特殊的类加载器来完成加载这种第三方提供实现的类库(线程上下文类加载器
)
从上图可以看出:
- 左侧为 双向委托模型
上面介绍了,接收到加载请求,向父类加载器委托,这里不重复了
- 右侧为 反双向委托模型
原因 由第三方提供了这些核心的SPI接口的实现,而
Bootstrap ClassLoader
并不能加载除了除了rt.jar
以外的包且受限双向模式,无法直接加载第三方实现提供的jar
包. 只能委托 (中间商)线程上下文类加载器
ContextClassLoader
去加载jdbc.jar
里面实现的SPI 核心接口的实现类
到内存中。其在执行过程中,抛弃了
双亲委派加载链
的模式,使得程序可以逆向使用类加载器,其实是对类加载器的一个补充,使得其更灵活
为什么使用 双亲委派机制
OK,回答这个问题,我们先假设并没有这个限制,在 rt.jar
中随便找一个类 HashMap
, 我同时在自定义的程序里面也写一个一样的类放在 ClassPath
下,那么这两个类就需要采用不同的类加载器,
那时候,系统中出现了两个不同的 HashMap
,这样会导致程序混乱。