类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析三个阶段统称为连接。
图中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序开始,但是解析不一定,解析可能出现在初始化之后,这是为了适配Java动态绑定的特性。
Java虚拟机规范规定了有且只有6中情况下必须对类立即进行”初始化“:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。典型场景有:
- 使用new 关键字实例化对象
- 读取或设置一个类型的静态字段(被final修饰的、已在编译期把结果放入常量池的静态字段除外)
- 调用一个类型的静态方法
- 使用
java.lang.reflect
包的方法对类型进行反射调用的时候 - 当初始化类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()
方法的类),虚拟机会先初始化这个类 - 如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
、REF_newInvokeSpecial
四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化 - 当一个接口中定义了默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
类加载过程
加载
加载阶段是整个类加载过程的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象
非数组类加载阶段既可以由Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成。
数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。但数组类与类加载器依然有很密切的关系,数组类的元素类型最终还是要靠类加载器来完成加载。
验证
验证是连接阶段地第一步,这一步是为了确保Class文件的字节流中包含的信息符合Java虚拟机规范的全部约束要求。
字节码可以在语义上表达出来Java语言所做不到的危险操作,所以验证阶段十分重要。
验证阶段大致分为四个阶段
- 文件格式验证
验证字节流是否符合Class文件格式规范
是否以魔数0xCAFEBABE
开头
主、次版本号是否在当前Java虚拟机接受范围之内
等 - 元数据验证
对字节码描述的信息进行语义分析
这个类是否有父类
这个类是否集成了不允许被继承的类
这个类是否实验写其父类或接口中要求实现的方法
等 - 字节码验证
通过数据流分析和控制流分析,确定程序语义是否合法、是否符合逻辑。 - 符号引用验证
对类自身以外的各种信息进行匹配性校验
准备
正式为类中定义的变量(即静态变量)分配内存并设置类变量初始值。
解析
Java虚拟机将常量池内的符号引用替换为直接引用。
- 符号引用:以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可
- 直接引用:可以使直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
初始化
执行类构造器 <cinit>()
方法的过程。
<cinit>()
方法是由编译期自动收集类中所有类变量赋值动作和静态语句块中的语句合并产生的,收集顺序和在类中的定义顺序相同,所以静态语句块只能访问到定位在它之前的静态变量,定义在它之后的变量可以对其赋值,但是不能访问。
public class Test {
static {
i = 0; // 可以编译通过
System.out.println(i); // 提示非法向前引用
}
static int i;
}
类加载器
类加载器的作用就是“通过一个类的全限定名来获取描述该类的二进制字节流”。
类与类加载器
比较两个类是否“相等”,只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
try{
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
}
运行结果
双亲委派模型
- 启动类加载器(Bootstrap Class Loader):这个类加载器由c++编写,是虚拟机的一部分,一般负责加载
<JAVA_HOME>\lib
目录下的能被Java虚拟机识别的类库。 - 扩展类加载器(Extension Class Loader):这个类加载器是以Java代码的形式实现的,负责加载
<JAVA_HOME>\lib\ext
目录的类库 - 应用程序加载器(Application Class Loader):负责加载用户类路径上所有的类库,这个也是程序中默认的类加载器
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先自己不会尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父加载器反馈复发完成这个加载请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载完成。
破坏双亲委派模型
双亲委派模型不是强制约束的模型,而是Java设计者推荐给开发者门的类加载器实现方式。所以有可能破坏双亲委派模型。如果用户覆盖了loadClass()
方法,也就是双亲委派机制实现的具体位置,那么类加载不会按照双亲委派模型来进行。