类加载时机
◆ ◆ ◆ ◆
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载;其中,验证、准备和解析统称为连接,如下图所示:
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。这里强调的是“开始”,而不是按部就班地“进行”或者“完成”,因为这些阶段通常是相互交叉地混合式进行,通常会在一个阶段执行地过程中调用或者激活下一个阶段。
以下四种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):
1遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
2.使用 java.lang.reflect 包的方法对类进行反射调用的时候。
3.当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
4.当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
类加载过程
◆ ◆ ◆ ◆
一、加载
在加载阶段,虚拟机需要完成三件事:
1.通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:
1.如果数组的组件类型是引用类型,那就递归采用类加载加载。
2.如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
3.数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在Java堆中实例化一个 java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。
二、验证
是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。
1.文件格式验证:验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。
(1)是否以魔数 0xCAFEBABE 开头
(2)主、次版本号是否在当前虚拟机处理范围之内
(3)常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
(4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
(5)CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
(6)Class 文件中各个部分及文件本身是否有被删除的附加的其他信息
……
只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。
2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求:
(1)这个类是否有父类(除 java.lang.Object 之外)
(2)这个类的父类是否继承了不允许被继承的类(final 修饰的类)
(3)如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
(4)类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
……
3.字节码验证:进行数据流和控制流分析。对类的方法体进行校验分析。这一阶段保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,如
(1)保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
(2)保证跳转指令不会跳转到方法体以外的字节码指令上
(3)保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
……
4.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段--解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常有以下内容:
(1)符号引用中通过字符串描述的全限定名是否能找到对应的类
(2)在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
三、准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法区中分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起被分配在Java堆中。
通常情况:
public static int value = 1234;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1234 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。
特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,如:
public static final int value = 1234;
编译时Javac将会为value生成ConstantValue属性,在准备阶段赋值为1234.
以下是基本数据类型的零值
四、解析
这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
1.符号引用:以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
2.直接引用:可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机的内存布局实现有关
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。
五、初始化
加载过程的最后一步,真正开始执行类中定义的Java代码,初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()需要 注意:
(1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块中可以赋值,但不能访问;
(2)<clinit>()方法与类构造函数<init>()不同,它不需要显示地调用父类构造器,虚拟机会保证在子类地<clinit>()方法执行之前,父类地<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行地<clinit>()方法的类肯定是java.lang.Object
(3)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值语句。
类加载器和双亲委派模型
◆ ◆ ◆ ◆
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
启动类加载器:负责将存放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
扩展类加载器:加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
应用程序类加载器:负责加载用户路径上所指的类库,开发者可以直接使用这个类加载器。
除顶层启动类加载器之外,其他都有自己的父类加载器。
加载工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载,如下图
摘自《深入理解Java虚拟机》
往期相关:
升职加薪,点击关注,不要走丢
无