共计 3281 个字符,预计需要花费 9 分钟才能阅读完成。
1 起源
- 起源:《Java 高并发编程详解 多线程与架构设计》,汪文君著
- 章节:第九、十、十一章
本文这三章的笔记整顿。
2 类加载简介
类加载的过程能够简略分为三个阶段:
- 加载阶段:次要负责查找并且加载类的二进制数据文件
- 连贯阶段:能够细分为验证、筹备、解析三个阶段,验证就是确保类文件的正确性,筹备就是为类的动态变量分配内存,并且为其初始化默认值,解析就是把类中的符号援用转换为间接援用
- 初始化阶段:为类的动态变量赋予正确的初始值
3 被动应用与被动应用
JVM
标准规定了每个类或接口在首次被动应用的时候都须要进行初始化,规定了以下六种被动应用类的场景:
- 通过
new
关键字会导致类的初始化 - 拜访类的动态变量
- 拜访类的静态方法
- 对某个类进行反射操作
- 初始化子类会导致父类初始化
- 启动类(就是蕴含
main()
的类)也会初始化
除了以上六种状况外,其余的都叫被动应用,不会导致类的加载和初始化,比方援用类的动态常量不会导致类的初始化。
4 类加载详解
后面也说了类加载能够简略分为三个阶段:
- 加载阶段
- 连贯阶段
- 初始化阶段
上面先来看一下加载阶段。
4.1 加载阶段
加载阶段就是将 class
文件中的二进制数据读取到内存之中,而后将该字节流代表的动态存储构造转换为办法区中运行时数据结构,并且在堆中生成一个该类的 java.lang.Class
对象,作为拜访办法区数据结构的入口。
类加载的最终产物就是堆内存中的 class
对象,JVM
标准中指出类加载是通过一个全限定名去获取二进制数据流,起源包含:
class
文件:这是最常见的格局,就是加载javac
编译后的字节码文件- 运行时动静生成:比方
ASM
能够动静生成,或者能够通过动静代理java.lang.Proxy
生成等 - 通过网络获取:比方
RMI
- 读取压缩文件:比方
JAR
、WAR
包 - 从数据库读取:比方读取
MySQL
中的BLOB
字段类型的数据 - 运行时生成
class
文件并且动静加载:比方Thrift
、Avro
等序列化框架,将某个schema
生成若干个class
文件并进行加载
类加载阶段完结后,JVM
会将这些二进制字节流依照 JVM
定义的格局寄存在办法区中,造成特定的数据结构后再在堆内存中实例化一个 java.lang.Class
对象。
4.2 连贯阶段
该阶段能够分为三个小阶段:
- 验证
- 筹备
- 解析
须要留神的是这三个小阶段其实不是程序进行的,而是交叉着进行的,也就是解析的时候其实也会有验证的过程。
4.2.1 验证
验证是为了确保字节流所蕴含的内容合乎 JVM
标准,并且不会呈现危害 JVM
本身平安的代码,当字节流信息不符合要求的时候,会抛出 VerifyError
这样的异样或其子异样,验证的信息包含:
- 文件格式
- 元数据
- 字节码
- 符号援用
4.2.1.1 验证文件格式
包含:
- 魔数(
0xCAFEBABE
) - 主次版本号
- 是否存在完好或附加信息
- 常量池常量类型是否反对
- 常量池援用是否指向不存在常量或不反对类型常量
- 其余
4.2.1.2 验证元数据
元数据验证其实是进行语义剖析的过程,语义剖析是为了确保字节流合乎 JVM
标准要求,包含:
- 查看某个类是否存在父类,是否继承某个接口,这些父类或接口是否非法,或是否存在
- 查看是否继承了
final
的类 - 查看抽象类,查看是否实现了父类的形象办法或接口办法
- 查看重载,比方雷同的办法名称、雷同的参数然而返回类型不同,这是不容许的
4.2.1.3 验证字节码
字节码验证次要是验证程序的管制流程,包含:
- 保障以后线程在程序计数器中的指令不会跳转到不非法的字节码指令中去
- 保障类型的转换是非法的
- 保障任意时刻虚拟机栈中的操作栈类型与指令代码都能正确被执行
- 其余验证
4.2.1.4 验证符号援用
验证符号援用转换为间接援用的合法性,保障解析动作的顺利执行,包含:
- 通过符号援用形容的字符串全限定名称是否可能顺利找到相干的类
- 符号援用中的类、字段、办法是否对以后类可见
- 其余
4.2.2 筹备
通过验证后,就开始了筹备阶段,这阶段比较简单,就是对对象的动态变量分配内存并且设置初始值,类变量的内存会被调配到办法区中。设置初始值就是为相应的类变量给定一个相干类型在没有被设置时的默认值,比方 Int
的初始值为 0,援用的初始值为null
。
4.2.3 解析
解析就是在常量池中寻找类、字段、接口和办法的符号援用,并且将这些符号援用替换成间接援用的过程。解析次要针对类接口、字段、类办法和接口办法进行的,包含:
- 类接口解析
- 字段解析
- 类办法解析
- 接口办法解析
4.3 初始化阶段
初始化阶段次要就是执行 <clinit>
办法的过程,该办法是编译阶段生成的,也就是说蕴含在字节码文件中,该办法蕴含了所有类变量的赋值动作和动态语句块的执行代码。另一方面,<clinit>
与构造方法不同,不须要显式调用父类结构器,虚构机会保障父类的 <clinit>
办法最先执行。
还须要留神的是 <clinit>
只能被虚拟机执行,虚拟机还会保障多线程下的安全性,因而,如果在动态代码块中如果蕴含了加载其余类的操作可能会引起死锁,例子能够看这里。
5 类加载器
5.1 JVM
中的三类外围类加载器
JVM
中有三类外围类加载器,别离是:
- 启动类加载器:启动类加载器是最顶层的类加载器,没有父加载器,由
C++
编写,负责JVM
外围类库的加载,比方加载整个java.lang
包中的类 - 扩大类加载器:扩大类加载器的父加载器是启动类加载器,次要加载
jre/lib/ext
子目录下的类库,纯Java
实现,是URLClassLoader
的子类 - 利用类加载器:也叫零碎类加载器,负责加载
classpath
下的类库,利用类加载器的父加载器为扩大类加载器,同时它也是自定义类加载器的默认父加载器
5.2 双亲委派机制
一个类加载器加载一个类的时候,并不会尝试间接加载该类,而是先交给父加载器尝试加载,始终到顶层的父加载器(启动类加载器),如果父加载器加载失败,则会本人尝试加载,图示如下:
6 线程上下文类加载器
JDK
中提供了很多 SPI
(Service Provider Interface
),比方JDBC
等,JDBC
只规定了这些接口之间的逻辑关系,但不提供具体的实现,换句话说,JDBC
齐全通明了应用程序和第三方厂商数据库驱动的具体实现,应用程序只须要面向接口编程即可。但问题是:
java.lang.sql
中的所有接口都是由JDK
提供的,加载这些接口的类加载器是启动类加载器- 第三方厂商的类库驱动由零碎类加载器加载
因为双亲委派机制,Connections
、Statement
等都是由启动类加载器加载,而第三方 JDBC
驱动包中的实现不会被加载。解决这个问题的要害,就是应用了线程上下文类加载器突破了双亲委派机制。
比方 MySQL
驱动的加载过程,就是通过线程上下文类加载器加载的,
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
//...
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {callerCL = Thread.currentThread().getContextClassLoader();}
while(true) {
//...
if (isDriverAllowed(aDriver.driver, callerCL)) {}}
//...
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
//...
try {aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception var5) {result = false;}
//...
return result;
}
通过线程上下文类加载器,就变成了启动类加载器去委托子类加载器去加载实现的形式,也就是 JDK
本人亲自突破了双亲委派机制这种形式,这种加载形式简直波及所有的 SPI
加载,包含 JAXB
、JCE
、JBI
等。