JVM类加载机制总结
1.运行时加载优点
提高灵活性,可以在运行时动态加载,连接。例子:面向接口编程,动态绑定实现类(但C++也有动态绑定,说明动态绑定不一定通过运行时加载Class字节码实现,也可能是机器码支持的)
2.编译并在运行时动态加载字节码优点
可在运行时动态获取二进制字节流作为动态代码,比如可以从网络上获取;可以做一个循环,动态从某文件库获取;可以从指定的ClassPath路径获取,比如一个jar包目录
3.与动态类型语言的区别
动态类型语言如JS,可在运行时添加类型属性,方法,在运行时改变类型行为
4.类加载生命周期
加载、验证、准备、解析、初始化、使用、卸载。按这个顺序开始,但交叉执行。验证、准备、解析统称为连接。解析阶段不一定按这个顺序,可以在初始化之后,为了支持上文的动态绑定(运行时绑定)。
5.必须“初始化”(类加载第5阶段)的几种情况(主动引用,有且仅有这些情况时要初始化,初始化钱当然要先“加载”,所以也是必须加载的几种情况)
new实例化对象、读取或设置类static非final字段(final static在编译时存入常量池)、调用类静态方法、类反射调用、初始化一个类时先初始化其父类、虚拟机启动的带main方法的执行主类(虚拟机执行入口)等。
6.不会激发“初始化”的情况(被动引用)
a.引用子类使用其父类的static非final字段(直接定义这个字段的类),只会初始化父类,不会初始化子类。子类是否加载、验证(类加载第1,2阶段),取决于虚拟机具体实现,无规定
b.通过数组定义引用类,不会触发该类初始化。这里触发了一个虚拟机自动生成的代表数组的类的初始化,里面有数组相关属性,比如public的length字段(数组长度)
c.一个类引用另一个类的public static final字段,不会初始化另一个类。该字段引用在编译时进入了前一个类(引用类)的常量池中,编译后就与后一个类完全没关系了。
d.接口初始化:不要求初始化其父类接口,这与类初始化不同。只有使用到父接口比如引用其常量时才会初始化父类接口
7.类加载过程
加载阶段:一个非数组类的加载是开发人员可控性最强的,可以使用系统的引导类加载器,也可使用用户自定义类加载器控制字节流获取方式(下面a所述),即重写loadClass方法。数组类由虚拟机创建,其元素类型由类加载器加载。
a.通过类全限定名获取其字节流,方式:zip/jar/war、网络、运行时计算生成(主要指动态代理生成的"*$Proxy"代理类Class文件)、其他文件生成(如jsp)、数据库读取等
b.字节流静态存储结构转换成方法区运行时数据结构
c.内存中生成代表该类的java.lang.Class对象,作为方法区类数据访问入口
验证阶段:
a.文件格式验证:如魔数开头、主次版本号、常量类型、不符合UTF8的编码
b.元数据验证:语义校验,比如是否有父类、是否继承final类(不允许继承)、是否实现接口、是否覆盖了父类final方法(不允许覆盖)
c.字节码验证:数据流与控制流验证,语义是否合法、符合逻辑。方法体校验:操作数栈数据类型是否与指令码使用类型匹配、指令是否跳到方法体外、方法体内类型转换是否有效
d.符号引用验证:符号引用转为直接引用是在解析阶段进行的,这里是对类自身以外(常量池中各种符号引用)的信息进行匹配校验,比如全限定名是否能找到该类、指定类是否有该符号引用所引用的方法和字段、引用的类、方法、字段访问性(private,protected,public,default)是否可被当前类引用,抛出对应IllegalAccessError、NoSuchMethodError等。
准备阶段:
在方法区将类变量(static)分配内存并设置初始值,通常赋类型零值。如public static int value=123;是赋值为0不是123.赋值123是putstatic指令,在类构造器<clinit>()中,是在初始化阶段才执行的。但public static int value=123;则在准备阶段赋值123.
解析阶段:
将常量池内符号引用替换为直接引用。符号引用:目标不一定已加载入内存。直接引用:直接指向目标的指针、相对偏移量或可间接定位的句柄。引入目标必定在内存中存在。
解析的7类符号引用:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
递归加载这些符号引用解析成直接引用后的那些类、父类、接口、父接口,进内存,然后使用直接引用指向它们。
初始化阶段:
执行类构造器<clinit>()方法的过程。
<clinit>()方法由编译器自动搜集所有类变量赋值动作和static{}块合并产生,顺序由源文件中出现顺序决定。static{}块可赋值在其后定义的变量,但不可访问。
public class Test {
static {
i = 0;//正常编译
System.out.println(i);//非法向前引用!!
}
static int i = 1;
}
父类<clinit>()方法先于子类<clinit>()执行,由虚拟机调用,而不是子类<clinit>()方法调用。因此父类静态块要优先于子类变量赋值动作。(值为多少的面试题)
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extents Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
B值为2.
接口没有静态块。<clinit>()方法用于变量初始化赋值。不需要先执行父类接口<clinit>()方法,使用时执行。实现接口的类初始化也不需要执行接口<clinit>(),使用时执行。
虚拟机保证<clinit>()方法多线程下安全。如多线程执行,其他线程会阻塞,但退出后其他线程也不会再进入执行。同一个类加载器下(一个类与其加载器一起确定唯一性),一个类型只初始化一次!