关于JVM 类加载的一些测试案例及说明
类加载的过程
类加载由7个步骤完成,看图
加载
1、通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)
2、解析成运行时数据,即instanceKlass实例,存放在方法区
3、在堆区生成该类的Class对象,即instanceMirrorKlass实例
何时加载
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行加载(加载、验证、准备都会随之发生),称为主动引用:
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有被加载,则需要先加载。
生成这4条指令的最常见的Java代码场景是:
- 使用new关键字实例化对象的时候
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有被加载,则需要先加载。
3、当加载一个类的时候,如果发现其父类还没有被加载,则需要先加载其父类。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先加载这个主类(当然如果主类存在未加载的父类,会先加载父类)。
5、当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有被加载,则需要先加载。
预加载:
包装类、String、Thread
因为没有指明必须从哪获取class文件,脑洞大开的工程师们开发了这些
1、从压缩包中读取,如jar、war
2、从网络中获取,如Web Applet
3、动态生成,如动态代理、CGLIB
4、由其他文件生成,如JSP
5、从数据库读取
6、从加密文件中读取
接下来直接上一些测试例子及结果说明:
测试代码1
public class Test_1 { public static void main(String[] args) { System.out.println(Test_1_B.str); while (true); } } class Test_1_A{ public static String str = "A str"; static { System.out.println("A Static Block"); } } class Test_1_B extends Test_1_A{ static { System.out.println("B Static Block"); } }
测试结果:
原因分析:
本示例看似满足加载时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未加载,则对该类进行加载。但对于静态字段,只有直接定义这个字段的类才会被加载,因此通过其子类来引用父类中定义的静态字段属于间接引用,只会触发父类的加载而不会触发子类的加载。
测试代码2
public class Test_2 { public static void main(String[] args) { System.out.printf(Test_2_B.str); } } class Test_2_A { static { System.out.println("A Static Block"); } } class Test_2_B extends Test_2_A { public static String str = "B str"; static { System.out.println("B Static Block"); } }
测试结果:
原因分析:
本示例满足加载时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未加载,则对该类进行加载。
本示例满足加载时机的第三条:当加载一个类的时候,如果发现其父类还没有被加载,则需要先加载其父类。
但对于静态字段,只有直接定义这个字段的类才会被加载。此示例对于静态字段的引用是直接引用,所以会触发子类的加载。
测试代码3
public class Test_3 { public static void main(String[] args) { System.out.printf(Test_3_B.str); } } class Test_3_A { public static String str = "A str"; static { System.out.println("A Static Block"); } } class Test_3_B extends Test_3_A { public static String str = "B str"; static { System.out.println("B Static Block"); } }
测试结果:
原因分析:
本示例满足加载时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未加载,则对该类进行加载。
本示例满足加载时机的第三条:当加载一个类的时候,如果发现其父类还没有被加载,则需要先加载其父类。
但对于静态字段,只有直接定义这个字段的类才会被加载。此示例对于子类的静态字段对父类的静态字段进行了覆盖。引用使用是直接引用,所以会触发子类的加载。在加载子类的时候,会优先加载其父类。
测试代码4
public class Test_4 { public static void main(String[] args) { Test_4[] arrs =new Test_4[1]; } } class Test_4_A{ static { System.out.println("Test_4_A Static Block"); } }
测试结果:
原因分析:
本示例满足加载时机的第四条:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先加载这个主类(当然如果主类存在未加载的父类,会先加载父类)。
虽然Test_4_A这个类和包含main方法的主类写在同一个文件中,但是编译器编译后,任然生产两个对立的class文件。虚拟机启动时,按需加载只会加载Test_4这个类。
测试代码5
public class Test_5 { public static void main(String[] args) { Test_5_A[] obj = new Test_5_A[1]; } } class Test_5_A { static { System.out.println("Test_5_A Static Block"); } }
测试结果:
原因分析:
本示例满足加载时机的第四条:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先加载这个主类(当然如果主类存在未加载的父类,会先加载父类)。
运行之后发现没有输出“Test_5_A Static Block”,说明并没有触发类cn.jvm.classload.Test_5_A的加载阶段。但是这段代码里面触发了另外一个名为 [Lcom.jvm.classload.Test_5_A 的类的加载阶段。对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令anewarray触发。
这个类代表了一个元素类型为cn.jvm.classload.Test_5_A的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法)都实现在这个类里。
简言之,现在通过new要创建的是一个Test_5_A数组对象,而非Test_5_A类对象,因此也属于间接引用,不会加载Test_5_A类。
测试代码6
public class Test_6 { public static void main(String[] args) { Test_6_A obj = new Test_6_A(); } } class Test_6_A { static { System.out.println("Test_6_A Static Block"); } }
测试结果:
原因分析:
测试代码7
public class Test_7 { public static void main(String[] args) { System.out.println(Test_7_A.str); } } class Test_7_A{ public static final String str = "A Str"; static { System.out.println("Test_7_A Static Block"); } }
测试结果:
原因分析:
测试代码8
public class Test_8 { public static void main(String[] args) { System.out.println(Test_8_A.uuid); } } class Test_8_A{ public static final String uuid= UUID.randomUUID().toString(); static { System.out.println("Test_8_A Static Block"); } }
测试结果:
原因分析:
本示例看似也满足加载时机的第一条:读取或设置一个类的静态字段,此属性被final修饰,应该不加载Test_8_A类。但是结果为什么会显示加载了Test_8_A类呢?
这是因为被final修饰的静态属性的值不是常量值,无法再编译时确定值,只有在运行是才能进行读取。所以需要加载Test_8_A类。详见下图:
测试代码9
public class Test_9 { static { System.out.println("Test_9 Static Block"); } public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz = Class.forName("com.jvm.classload.Test_1_A"); } }
测试结果:
原因分析:
本示例满足加载时机的第二条:使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有被加载,则需要先加载。
本示例满足加载时机的第四条:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先加载这个主类(当然如果主类存在未加载的父类,会先加载父类)。
测试代码10
public class Test_10 { public static void main(String[] args) { System.out.println(new Test_10_B().str); } } class Test_10_A{ static { System.out.println("A Static Block"); } } class Test_10_B extends Test_10_A{ public String str="B Str"; static { System.out.println("B Statci Block"); } }
测试结果:
原因分析:
本示例满足加载时机的第一条:使用new关键字实例化对象的时候,如果类没有被加载,则需要先加载。
本示例满足加载时机的第三条:当加载一个类的时候,如果发现其父类还没有被加载,则需要先加载其父类。