虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的
类的生命周期
PS:
NoClassDeFoundError:
发生在类生命周期中解析阶段找不到相应的类
ClassNotFoundException
发生在类生命周期的加载阶段,找不到相应的类。
为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,除解析过程外的其他加载过程必须按照如图顺序开始。
静态存储结构转化为方法区
的运行时数据结构。生成一个代表这个类的 java.lang.Class 对象
,作为方法区这个类的各种数据的访问入口。验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
在解析阶段中发生
,保证可以将符号引用转化为直接引用。可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
为
类变量分配内存并设置类变量初始值
,这些变量所使用的内存都将在方法区中进行分配。
public static int value = 123
在准备阶段过后的初始值是0 而不是123,因为这个时候没有正在的执行过java代码;而把value赋值为123的putstatic
指令是程序被编译后,存放在类构造器clinit()
方法中,将会在初始化的时候执行
特殊情况:final修饰
public final static int value = 123
被final修饰之后就是常量属性,在准备阶段虚拟机就会直接赋值,不需要等到初始化阶段;
虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,除解析过程外的其他加载过程必须按照如图顺序开始
NoClassDeFoundError:
发生在类生命周期中解析阶段找不到相应的类
到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行
clinit()
方法的过程。 clinit() 方法是由编译器按语句在源文件中出现的顺序
,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的
。(不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 init() 方法来初始化对象)
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问 如下:
这样的写法是正常的; 但是如果把声明换个位置
这样就会提示 非法前向引用
; 进而说明 clinit() 方法是由编译器按语句在源文件中出现的顺序 依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的
总结一下:
对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
双亲委派模型的加载类逻辑可参考如下代码:
// 代码摘自《深入理解Java虚拟机》
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}