why
为什么要进行类加载?
- 编译后的Class文件并不能直接被JVM使用
- Class文件是对类描述的一段二进制字节流
- JVM是一个进程,只能对内存中的数据进行操作
要将Class文件加载到JVM中,然后根据描述在不同的内存空间给它分配内存
类加载步骤
- 加载
-
连接
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
一、加载
作用
将二进制字节流存储在方法区中,然后在堆内存中实例化一个Class类对象,这个对象作为访问方法区中的类型数据的外部接口
特性
-
这是可控性最强的阶段,这个阶段可以自定义很多自己的东西
- 安全:加载加密Class文件,避免程序逻辑曝光
- 动态代理技术:编写反射接口,在运行时计算生成对象
- 从其他文件生成:由JSP文件生成对应的Class文件
- Class文件不一定是存在磁盘,只是指一段二进制字节流
- RPC框架的原理是将Class文件传过去,在另外一端进行加载并使用
- 同一个Class文件经过不同的类加载器加载,得到的对象是不同的
- 加载阶段和连接阶段是交叉进行的
类加载条件
- 通过new实例化对象
- 反射调用
- 实例化子类的时候,会先实例化父类
- JVM启动的时候会先加载main函数所在的主类
证明间接引用不会触发类加载
添加虚拟机启动参数打印加载阶段的信息
-XX:+TraceClassLoading
1. 通过子类引用继承的静态成员变量,不会触发子类初始化
public class NotInitialization{
public static void main(String []args){
System.out.print(SubClass.value);
}
}
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public final static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
结果打印:
SuperClass init!
2. 通过定义数组,并不会初始化对象
public class NotInitialization{
public static void main(String []args){
SuperClass []superClasses = new SuperClass[10];
}
}
结果:没有显示init
3. 应用类的静态常量,不会触发该类的加载
public class NotInitialization {
public static void main(String[] args) {
System.out.print(SuperClass.value);
}
}
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public final static int value = 123;
}
结果:没有显示init
原因:编译阶段会做常量传播优化,将vale的值存在NotInitialization类对应的常量池中,执行main方法相当于仅调用自身常量池的引用,且不持有SuperClass的引用
二、连接
1. 验证
确保加载进来的Class文件字节流符合规范,不会危害JVM安全
可通过 -Xverify:none 参数来关闭大部分类验证,缩短短类加载时间
特性
与加载阶段交叉运行,最耗费时间的
① 文件格式校验
- 保证字节流符合Class文件规范,并且可以被当前JVM处理
- 只有文件格式校验通过,这段字节流才允许存储在方法区中
- 后面三个验证阶段都是直接验证的方法区,不会再操作字节流
② 元数据验证
对字节码的描述进行语义校验
- 是否有父类
- 是否继承了final类
- 如果这个类不是抽象类,是否实现了父类/接口要求实现的方法
- 重载/复写是否符合规范,如覆盖父类的final字段、参数列表相同但返回类型不同
③ 字节码验证
通过数据流和控制流分析确保语义合法且符合逻辑,确保其不会危害虚拟机
- 确保所有跳转指令不会跳转到方法体意外的字节码指令
- 确保方法体中类型转化是有效的,避免把对象赋值给跟它不相干的数据类型
特性
① 最耗费时间
② 无法确保字节码验证后的代码没问题,程序无法准确判断
③ 在javac编译器里加了StackMapTable优化字节码验证时间
④ 符号引用验证
保证解析行为能够正常执行
特性
① 检查是否能根据符号引用中的全限定名找到对应的类
② 符号引用中的类、字段、方法的是否可被当前类访问(权限规则)
2. 准备
给静态变量分配内存并进行初始化
特性
① java7以及以前是在方法区中分配,之后是配置在java堆中
② 静态常量的话会在这里进行初始化并且赋值,静态变量的话仅初始化(赋0值)
3. 解析(重点)
将常量池中的符号引用转为直接引用
-
符号引用:java类在编译时并不知道引用对象的内存地址,就用符号表示
- 即便是引用自身的成员变量也是符号引用
- 引用的目标不一定是已经加载到内存的内容
-
直接引用:直接指向目标的指针
- 引用的目标一定在虚拟机中存在
- 性能比符号引用快
三. 初始化
真正执行类中编写的java代码
特性
① 初始化前由类加载器主导,从初始化开始由程序主导
② 初始化阶段就是执行类构造器的构成
双亲委派模型
每个类加载器在收到类加载请求时,会优先委派给父类加载器,只有当父加载器在自己的搜索范围没有找到所需的类,子加载器才会尝试自己去加载
作用
确保基础类是相同的类加载器加载,保证java程序的稳定运行
避免不同的类加载器去加载常用的类如Object,会导致应用程序混乱
三层类加载器
- 启动类加载器
负责加载lib下jvm能识别的jar - 扩展类加载器
负责加载libext目录中的jar包,一般存放通用的jar - 应用程序类加载器
负责加载用户类路径所有的类库
特性
- java一直保持三层类加载器,双亲委派的类加载架构
- 类加载器由两种组成
① 启动类加载器:Bootstrap 有C++实现,是虚拟机的一部分和其他类加载器
② 其他类加载器由Java语言实现,并且都继承java.lang.ClassLoader组成
破坏双亲委派模型
为什么要破坏
弄懂OSGi的实现
应用场景
- OSGi热部署
- 代码热替换
问题
1. 类在什么情况下会卸载?
答:同时满足这三个条件 ① 这个类的所有实例都被回收 ② 加载该类的ClassLoader已被回收 ③ 该类对应的java.lang.Class对象没有被任何地方引用,无法通过反射创建对象
类卸载就是在方法区中清空该类的信息,java8及之后永生带消除,里面存放的出数据也就是类的信息被移动到java堆
2. 连接的阶段的各个细节到底都做了些什么?
答:验证、准备、解析
3. 什么是双亲委托机制?好处?什么场景下需要避开这个机制?
答:因为不同加载器加载同一个类出来的对象是不相等的,假如用多个加载器加载Object,会导致代码比较混乱。所以需要确保基础用的类都是相同的类加载器加载。双亲委派模型就是指每个类加载器在收到加载类的请求时,会优先委托父加载器加载,除非父加载器无法加载,才会自己尝试加载。热部署的情况下需要避开
4. 怎么证明类就是按照你说的那样加载?如何证明?
答:具体加载的步骤无法看到,只能通过加-XX:+TraceClassLoading查看是否被加载
小结
- 熟悉类加载机制可以引导开发者更规范的编写代码
- 熟悉类加载机制可以根据业务编写更灵活的代码(OSGi)
- 熟悉类加载机制可以快速定位问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。