✝️ 类加载
2022年6月20日
- Java
✝️ 类加载
1. 什么是类加载
虚拟机的类加载机制
- Java 虚拟机将描述类的数据从
Class
文件加载到内存, - 并对数据进行 校验、转换解析 & 初始化
- 最终形成能被虚拟机直接使用的 Java 类型
2. 类型的生命周期
[注:] “类型”是指 类 | 接口,之后统称为 类
类的生命周期
后面打 Y 表示类加载过程中,开始的顺序确定,但并不意味着前一个没有结束就不能开始后一个
1) 加载
- 任务:
- 通过类的
全限定名
来获取定义该类的二进制字节流
- 可从 zip 压缩包、 jar、 war、加密文件等中获取二进制字节流
- 将二进制字节流所代表的
静态存储结构
转化为方法区的运行时数据结构
- 将 Class 文件中的类型信息,例如有哪些常量、字段有哪些、方法有哪些这些信息放入方法区中
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口- 这时还没有调用 new 创建对象,为什么就在堆上生成了一个对象?
- 注意这里生成的是 java.lang.Class 对象,它是为了反射时用的
- 通过类的
Hello obj = new Hello();
Class<?> clz = obj.getClass();
- 特点:
- 非数组类型的加载阶段是开发人员可控性最强的阶段
- 即可以使用虚拟机内置的启动类加载器完成,还可以自定义类加载器完成
- 数组类的加载
- 数组类本身不通过类加载器创建,而是又 Java 虚拟机直接在内存中动态构造生成
- 但数组内部元素所在的类仍需要通过类加载器来完成加载
- 如何实现从网络上加载? - 来自
ClassLoader
类的示例
ClassLoader loader= new NetworkClassLoader(host,port);
Object main= loader.loadClass("Main", true).newInstance();
class NetworkClassLoader extends ClassLoader {
String host;
int port;
// 重写 findClass 方法
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// 自定义方法,实现根据名称从网络上加载该类,得到 byte[] 数组
private byte[] loadClassData(String name) {
// load the class data from the connection
...
}
}
2) 验证
- 目的:
- 确保
Class
文件的字节流中的信息符合虚拟机规范,不会对虚拟机产生危害
- 确保
- 验证内容:
- 文件格式的验证
CA FE BA BE
开头- 次、主版本号是否在有效范围内
- 常量池中的常量类型是否都能识别,常量的索引值能否正确指向等
- 元数据的验证
- 这个类是否有父类,除 java.lang.Object 外,所有类均应有父类
- 是否继承了不能被继承的类 - final 类
- 是否为抽象类,是否实现了父类或接口中要求实现的所有方法
- 类中是否覆盖父类的 final 字段,是否出现不符合规则的方法重载
- 字节码验证
- 对类的方法进行校验,即 Class 文件中的 Code 属性,确保方法的跳转、类型转换等无误
- 符号引用验证
- 能否通过符号引用中的类的全限定名访问到需要的外部类
- 能否访问需要的外部类的字段 & 方法
- 文件格式的验证
3) 准备
- 任务:
- 为类静态变量分配内存 & 赋初值
- 补充:
- 分配到哪里的内存
- 理论上说,类静态变量属于方法区
- 但方法区只是一个逻辑概念
- JDK 7 时将 类静态变量移到了堆中,因此此时分配内存是分配到了堆内存中
- 赋初值
- 这里的初值的指,类静态变量所属的类的初始值,例如 0 值
- 而赋实际值的操作发生在初始化阶段的
<clinit>()
- 但特殊情况下,若类静态变量被
final
修饰,则会直接赋实际值
4) 解析
- 任务:
- 将常量池中的
符号引用
转换为直接引用
的过程- 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要能无歧义定位到目标即可
- 直接引用:可以直接指向目标的指针 | 相对偏移量 | 一个间接定位到目标的句柄
- 将常量池中的
- 解析内容:
- 主要针对 类或接口、字段、方法、方法句柄、调用点限定符的相关符号引用进行解析
- 解析前提:
- 并不能将所有符号引用转换为直接引用,
- 只有方法在程序真正运行之前就能确定调用哪个版本时,即运行期间不可改变的这部分方法的调用才能被解析出来
- 必须满足 编译期已知,运行期不可变
- 而满足这一前提的方法,只有:
静态方法
- 与类直接关联 - 字节码指令invokestatic
私有方法
- 只有自身类可访问,外部类不可访问 -invokespecial
实例构造器
-<init>()
-invokespecial
父类中的方法
-invokespecial
- 被
final
修饰的方法 -invokevirtual
- 而满足这一前提的方法,只有:
- 举例:
5) 初始化
- 目标:
- 执行类构造器
<clinit>()
的过程
- 执行类构造器
- 初始化时机:
- 含
main
的主类 - 遇到
new
|getstatic
|putstatic
|invokestatic
这四条字节码指令时- 遇到 new 关键字实例化对象时
- 读取 | 设置一个类的非
final
修饰的类静态变量时 - 调用一个类的静态方法时
- 使用 java.lang.
reflect
包下的方法对类进行反射调用时,若该类未初始化,则需要先初始化 - 初始化类时,发现父类还未进行初始化,先初始化父类
- 含
<clinit>()
- 如何生成:
- 是
Javac
编译期自动收集类中的所有类静态变量的赋值操作 & 静态代码块的语句合并生成 - 若没有 类静态变量 | 静态代码块,那么不会生成
<clinit>()
- 是
- 收集顺序:
- 静态语句块中只能访问到定义在它之前的变量
- 定义在它之后的变量,在前面的静态代码块中可以赋值,但不可访问
- 执行顺序:
- Java 虚拟机会保证该类的
<clinit>()
执行之前,父类的<clinit>()
已经执行完毕 - 因此 第一个被执行的
<clinit>()
的类肯定是java.lang.Object
- Java 虚拟机会保证该类的
- 是否必须:
- 若一个类中并没有静态语句块,也没有类静态变量的赋值操作,则编译器不会为这个类生成
<clinit>()
- 若一个类中并没有静态语句块,也没有类静态变量的赋值操作,则编译器不会为这个类生成
- 接口的特殊情况
- 接口中没有静态代码块,但可能有静态变量的赋值操作,因此也可能生成
<clinit>()
- 接口执行
<clinit>()
时,无需先执行父类的<clinit>()
,只有父接口定义的变量被使用时,父接口才会进行初始化 - 接口的实现类在初始化时也不会执行接口的
<clinit>()
- 接口中没有静态代码块,但可能有静态变量的赋值操作,因此也可能生成
- 线程安全
- Java 虚拟机必须保证 一个类的
<clinit>()
在多线程下的加锁同步,只有一个类能够去执行这个类的<clinit>()
,其他线程阻塞等待直到完成 - 其他线程唤醒后,不会再次进入
<clinit>()
,同一类加载器下,一个类只会被初始化一次
- Java 虚拟机必须保证 一个类的
3. 双亲委派模型
1) 类加载器
- 通过类的
全限定名
来获取定义该类的二进制字节流
,实现这个操作的代码被称为 “类加载器”- 比较 2 个类是否 “相等”,必须在这两个类由同一类加载器加载的前提下,才有效
- 否则,即使这两个类来自于同一个 Class 文件,且被同一个 Java 虚拟机加载,若是类加载器不同,则这两个类必不相等
- “相等” 指的是代表该类的 Class 对象的 equals() isInstance() 等的返回结果相等
2) 双亲委派模型
① 三层类加载器
- 启动类加载器 [
Bootstrap Class Loader]
- 负责加载存放在
<JAVA_HOME>\lib
目录 |-Xbootclasspath
参数指定的路径中存放的,且为 Java 虚拟机识别[根据名称]的类库 - HotSpot 中使用
C++
实现,J9
&JRockit
中虽然有Java
类的存在,但关键方法仍是通过 JNI 回调C
语言
- 负责加载存放在
- 扩展类加载器 [
Extension Class Loader
]- 以 Java 代码实现,可直接在程序中使用扩展类加载器来加载 Class 文件
- 负责加载存放在
<JAVA_HOME>\lib\ext
目录 | 被java.ext.dirs
系统变量指定路径中的所有类库
- 应用程序类加载器 [
Application Class Loader
]- 负责加载用户类路径 [
ClassPath
] 上的所有类库 - 可直接在代码中使用这个类加载器
- 若没有自定义类加载器,那么一般情况下,该类加载器就是程序中默认的类加载器
- 负责加载用户类路径 [
② 双亲委派模型如下:
③ 双亲委派模型的代码实现
public abstract class ClassLoader {
private final ClassLoader parent;
// ...
// 使用 指定的二进制名称 加载类
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// 使用 指定的二进制名称 加载类
// 合法的 二进制名称 如下:
// "java.lang.String"
// "javax.swing.JSpinner$DefaultEditor"
// "java.security.KeyStore$Builder$FileBuilder$1"
// "java.net.URLClassLoader$3$1"
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
// 2. 类未被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 2.1 父加载器不为空,调用父加载器 loadClass() 方法处理
c = parent.loadClass(name, false);
} else { // 2.2 父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 3. 如果仍未成功加载,调用 findClass(name) 尝试
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 返回类加载操作的锁对象
// 若此类加载器对象注册时具有并行能力,则该方法将返回与指定类名关联的专用对象;否则,该方法返回 当前 类加载器对象
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
// 查找具有指定二进制名称的类,可被重写
// 可通过重写该方法,在双亲委派的情况下,仍无法加载该类时,从 网络 | 压缩件 等文件中加载该类
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
④ 双亲委派模型优点:
- Java 中的类随着类加载器一起具备了优先级的层次关系,防止同一个类被不同类加载器加载后得到不相等的结果,保证核心 API 不被篡改
- 例如, java.lang.Object 存在于 rt.jar 中,不论被哪种类加载器加载,最后总能被启动类加载器加载,从而保证同一性
⑤ 打破双亲委派
- Tomcat 打破双亲委派
- 背景
- 主流 Web 服务器应能够解决以下问题:
- 部署在同一服务器下的 2 个 Web 应用程序所使用的的 Java 类库可以相互隔离
- 部署在同一服务器下的 2 个 Web 应用程序所使用的的 Java 类库可以相互共享 - 节约资源
- 服务器应尽可能保证自身的安全不受部署的 Web 应用程序的影响
- 支持 JSP 应用的 Web 服务器,几乎都支持
热替换
功能- 一种动态网页开发技术。它使用JSP标签在HTML网页中插入Java代码。标签通常以
<%
开头以%>
结束
- 一种动态网页开发技术。它使用JSP标签在HTML网页中插入Java代码。标签通常以
- 如何打破
- Tomcat 总是先尝试去加载某个类,如果找不到再用上一级的加载器,跟双亲加载器顺序正好相反