类加载的三个阶段
- 加载(Loading): 查找并加载类的二进制数据
- 链接(Linking):
- 验证(Verifying):确保被加载类的正确性(防止恶意文件被JVM加载
- 准备(Preparing):为类的静态变量分配内存,并将其初始化为默认值
- 解析(Resolving):把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
主动使用的分类
new
,直接使用- 访问某个类或接口的静态变量,或者对该静态变量进行赋值操作
- 调用静态方法
- 反射某个类
- 初始化一个子类
- 启动类, 比如
java HelloWorld
加载类的方式
- 本地磁盘中直接加载
- 内存中直接加载
- 通过网络加载.class
- 从zip,jar等归档文件中加载.class
- 数据库中提取.class文件
- 动态编译
剖析类加载的过程
加载
加载的最终产物是位于堆区的Class对象
类的二进制文件存储在方法区中(数据结构),然后再堆区中创建一个Class对象,这个对象作为程序访问方法区中这些数据结构的外部接口
链接
验证
- 魔术因子
0xCAFEBABY
- 主从版本号(当前虚拟机能否支持启动这个类)
- 常量池中的常量类型
- 元数据验证
- 是否有父类
- 父类是不是允许继承
- 是否实现了抽象方法
- 是否覆盖了父类的
final
字段 - 其他的语义检查
- 字节码验证
- 符号引用验证
准备
准备阶段就是给类变量
分配内存,并初始化默认值
解析
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
在这一步会将符号引用转换为直接引用
比如调用System.out.println()
会生成get stack system.out
类似的字眼
初始化
执行构造函数<clinit>()
虚拟机保证初始化对象时是线程安全的, 即一个类有两个线程同时初始化对象,只有一个线程能进
JVM类加载器介绍
根类加载器
获取核心类库
System.out.println(System.getProperty("sun.boot.class.path"));
结果
D:Javajdkjrelib
esources.jar;D:Javajdkjrelib
t.jar;D:Javajdkjrelibsunrsasign.jar;D:Javajdkjrelibjsse.jar;D:Javajdkjrelibjce.jar;D:Javajdkjrelibcharsets.jar;D:Javajdkjrelibjfr.jar;D:Javajdkjreclasses
扩展类加载器
获取扩展类库
System.out.println(System.getProperty("java.ext.dirs"));
结果
D:Javajdkjrelibext;C:WINDOWSSunJavalibext
双亲委派机制
- 父子类加载器之间的真实关系- 包装关系
- 父委托机制的优点就是能够提高系统的安全性,在此机制下,用户自定义的类加载器不可能加载 应该由父加载器的可靠类,因此可以防止恶意的代码代替父加载器的可靠代码
看源码知过程
- 首先检查当前类加载器是否已经缓存过类
- 发现父加载器,则递归进行加载
- 如果当前不存在父加载器,直接调用根加载器对类进行加载
- 如果当前所有父类都没加载成功,那么直接调用当前的类加载器进行加载
- 如果类被成功加载,做一些性能数据的统计
如何改变赴委托机制
重写loadClass()
方法
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
if (name.startsWith("java.")) {
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (Exception e) {
//ignore
}
}
try {
clazz = findClass(name);
} catch (Exception e) {
e.printStackTrace();
}
if (clazz == null && getParent() != null) {
getParent().loadClass(name);
}
return clazz;
}
双亲委托机制好处
- 保护正常系统类的安全性
- 避免有些类被重复加载
命名空间
命名空间是由该加载器以及所有父加载器所加载的类构成的。
运行时包
由同一类加载器加载的属于相同包的类组成了运行时包。(包+类加载器) 只有属于同一运行时包的类间才能相互访问可见。
由于java.lang.TestCase
和核心类库java.lang.*
由不同的类加载器加载,他们属于不同的运行时包,所以,java.lang.TestCase
不能访问java.lang
包中的包可见成员。
类卸载
满足下面三个条件才会被GC
- 该类的所有实例都已经被GC
- 加载该类的ClassLoader实例被回收
- 该类的class实例没有在其他地方被引用