关于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()方法的那个类),虚拟机会先加载这个主类(当然如果主类存在未加载的父类,会先加载父类)。

这个过程看似也满足加载时机的第一条:遇到new创建对象时若类没被加载,则加载该类。
运行之后发现没有输出“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");
    }
}

测试结果:

 

 原因分析:

本示例满足加载时机的第一条:使用new关键字实例化类对象的时候,如果该类尚未加载,则对该类进行加载。
 

测试代码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");
    }
}

测试结果:

 原因分析:

本示例满足加载时机的第一条:读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
Test_7_A类在编译时已经将静态属性的常量值存入常量池,Test_7在编译时也直接指向了常量池。所以不需要加载Test_7_A类。见下图:

 

 

 

 

测试代码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关键字实例化对象的时候,如果类没有被加载,则需要先加载。

本示例满足加载时机的第三条:当加载一个类的时候,如果发现其父类还没有被加载,则需要先加载其父类。

posted on 2020-08-03 21:40  AlexGeng  阅读(424)  评论(0编辑  收藏  举报

导航