JVM类加载机制
类的生命周期
加载——验证——准备——解析——初始化——使用——卸载
其中:加载、验证、准备、初始化、卸载是严格有序的
类加载时机:
这里的类加载时机指使类进入生命周期的时机,而不是第一个阶段的加载。
类的主动加载当且仅当五种情况时主动发生:
1、遇到new、getstatic、putstatic、invokestatic字节码(特殊情况见下面的验证)
2、被反射调用
3、子类被初始化时其父类会主动加载
4、含main方法的主类在启动时会被加载
5、使用动态语言支持的某些情况下
除此之外引用类的方法都不会发生类的加载
可以通过静态初始化块总是在类加载进行初始化时被调用的特性来验证。
这里定义了三个类以此来说明验证部分情况:
public class Test { //public final static int SIZE = 1; publicstaticintSIZE = 1; //静态初始化块将在类被初始化时调用 static{ System.out.println("test init"); } } public class Test2 extends Test{ static { System.out.println("test2 init"); } } /** * main方法中三行验证需要更具情况注释掉其中的一行或者两行 * * @author shiin * */ public class Test3 { publicstaticvoid main(String[] args) { //直接通过Test调用静态变量SIZE 定义了该静态变量的Test类将被初始化 intsize = Test.SIZE; //通过Test子类Test2调用静态变量SIZE 定义了该静态变量的Test类将被初始化其子类不会被初始化 //一旦此处的SIZE被final修饰,SIZE成了编译期进入常量池的静态常量,此时无论通过Test还是Test2都无法导致类的初始化 intsize1 = Test2.SIZE; //定义类的数组不会导致类初始化 Test2[] t2 = new Test2[5]; } }
类的加载过程:
加载:
1、通过类全名获取类的二进制字节流
2、通过二进制字节流获得其数据结构写入并写入方法区
3、在内存中生成一个该类的类对象,作为各种数据访问的入口
验证:
在确信代码正确性的前提下可以考虑使用虚拟机参数 -Xverify:none 来关闭大部分的验证措施
1、文件格式验证
2、元素据验证
3、字节码验证
4、符号引用验证
准备:
1、为静态类变量分配内存
2、设置静态类变量的初始值为对应类型的零值
注:final static 修饰的类静态常量(限拥有ConstantValue属性的类型)将在准备阶段被赋初始值而不是零值
解析:
将常量池内的符号引用替换为直接引用
此过程包括对各种类(接口),方法(接口方法及类方法),字段的解析
初始化:
该阶段会执行类构造器<clinit()>方法
<clinit()>由所有类变量赋值动作和静态代码块合成的方法
该方法中的执行顺序是由源文件中出现顺序决定,因此先出现的静态代码块不能访问到后定义的类变量,但可以在先出现的静态代码块中定义后出现的变量,举个例子说:
public class Test { public static void main(String[] args) { } static { SIZE = 0; } public static int SIZE ; static{ System.out.println(SIZE); } }
此程序将在控制台输出0,但如果试图在第一个静态代码块中加入System.out.println(SIZE),程序将会报错,提示Cannot reference a field before it is defined ,意味无法在定义前引用该变量
虚拟机会保证父类的<clinit()>方法在子类<clinit()>之前执行,且类的实例构造方法<init>()一定是后于该类的<clinit()>方法执行的
类构造器<clinit()>方法并不是一定会生成的,在没有成员变量赋值操作以及静态代码块时,该方法可以不被生成
接口中不能有静态代码块,但可以为成员变量赋值,且接口的类构造器<clinit()>方法只在实现了该接口的类使用了接口中的变量时才会执行该接口的<clinit()>方法
多个线程调用同一个类的<clinit()>方法时,<clinit()>方法会加锁同步,且同一个<clinit()>方法只会执行一次,第二个线程初始化该类时将不会调用<clinit()>方法