虚拟机类加载机制总目录

来源:互联网 发布:富善投资知乎 编辑:程序博客网 时间:2024/06/06 17:23

1.概述

在上一篇Class文件结构中,我们了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。
本篇主要讲解:

  1. 虚拟机如何加载这些Class文件?
  2. Class文件中的信息加载到虚拟机后会发生什么变化?

2.虚拟机类加载机制

  1. 编译型语言:首先将源代码编译生成机器语言,再由机器运行机器码(二进制)。运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、Delphi等
  2. 解释型语言:源代码不是直接编译成机器语言,而是先编译成中间代码,再由解释器对中间代码进行解释运行
    这里写图片描述

2.1 虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

2.2 动态加载和连接

编译型语言在编译阶段会进行连接工作,在Java里,类型的加载、连接和初始化都是在程序运行期间完成的。
这种策略的优缺点:
5. 缺点:在类加载时增加了性能开销
6. 优点:为Java应用程序提供了高度的灵活性

2.3 动态扩展

Java天生可以动态扩展的语言特性就是依赖动态加载和动态连接这个特点实现的。

3.类加载的时机

3.1 类的生命周期

这里写图片描述

3.2 加载阶段

Java虚拟机规范中没有强制约束什么时候进行类加载过程的第一个阶段:加载,由虚拟机的具体实现来控制

3.3 初始化阶段

3.3.1 主动引用

Java虚拟机规范严格地、强制地规定了 有且只有 5种 情况 必须 立即 对类进行初始化:
1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。
4. 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
有且只有这5种情况中的行为称为对一个类进行主动引用
这里写图片描述

3.3.2 主动引用实例

package org.fenixsoft.classloading;/** * new : 使用new关键字实例化对象 * 如果本类没有进行过初始化,则需要先触发其初始化 * */public class NewTest {    static {        System.out.println("第一种情况:遇到new字节码指令 - 使用new关键字实例化对象");    }}
package org.fenixsoft.classloading;/** * getstatic : 获取一个类的静态字段 * 如果本类没有进行过初始化,则需要先触发其初始化 * */public class GetStaticTest {    public static String staticField = "getstatic";    static {        System.out.println("第一种情况:遇到getstatic字节码指令 - 获取一个类的静态字段");    }}
package org.fenixsoft.classloading;/** * putstatic : 设置一个类的静态字段 * 如果本类没有进行过初始化,则需要先触发其初始化 * */public class PutStaticTest {    public static String staticField = "putstatic";    static {        System.out.println("第一种情况:遇到putstatic字节码指令 - 获取一个类的静态字段");    }}
package org.fenixsoft.classloading;/** * invokestatic : 调用一个类的静态方法 * 如果本类没有进行过初始化,则需要先触发其初始化 * */public class InvokeStaticTest {    static {        System.out.println("第一种情况:遇到invokestatic字节码指令 - 调用一个类的静态方法");    }    public static void invokeStatic () {    }}
package org.fenixsoft.classloading;/** * getstatic : 获取一个类的静态字段 * 被final关键字修饰的静态字段,不需要进行初始化,因为在编译期已经存入了常量池 * */public class GetFinalStaticTest {    public final static String staticField = "getstatic";    static {        System.out.println("final修饰的字段会在编译期就放入了常量池,不需要进行初始化");    }}
package org.fenixsoft.classloading;/** * 主动引用:有且只有5种情况 必须 立即 对类进行初始化 * */public class ActivaRefrence {    static {        System.out.println("第四种情况:虚拟机启动时,首先初始化主类 - 含main方法的那个类");        System.out.println();    }    /**     * 第四种:虚拟机启动时执行主类     * */    public static void main(String[] args) {        // 第一种:遇到new字节码指令 - 使用new关键字实例化对象        NewTest newTest = new NewTest();        // 第一种:遇到getstatic字节码指令 - 获取一个类的静态字段        String getStaticField = GetStaticTest.staticField;        // 第一种:遇到putstatic字节码指令 - 获取一个类的静态字段        PutStaticTest.staticField = "setStaticField";        // 第一种:遇到invokestatic字节码指令 - 调用一个类的静态方法        InvokeStaticTest.invokeStatic();        // 获取一个类的被final修饰的静态字段,不会触发其初始化,        // 不会输出        String getFinalStaticfield = GetFinalStaticTest.staticField;    }}

执行结果:

第四种情况:虚拟机启动时,首先初始化主类 - 含main方法的那个类第一种情况:遇到new字节码指令 - 使用new关键字实例化对象第一种情况:遇到getstatic字节码指令 - 获取一个类的静态字段第一种情况:遇到putstatic字节码指令 - 获取一个类的静态字段第一种情况:遇到invokestatic字节码指令 - 调用一个类的静态方法

这里写图片描述

3.3.3 被动引用

除了上述5种主动引用外,所有引用类的方式都不会触发初始化,称为被动引用

3.3.4 被动引用实例

3.3.4.1 子类引用父类的静态字段,不会导致子类初始化

对于静态字段,只有直接定义这个字段的类才会被初始化

  • 因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  • 而子类引用子类中静态字段,会触发父类的初始化,因为根据第一种场景,获取一个类的静态字段必须立即初始化类,又根据第三种场景,子类在初始化时,如果父类没有初始化过,需要先触发父类的 初始化。
  • 至于是否要触发子类的加载阶段验证阶段,在虚拟机规范中并未明确规定,取决于虚拟机的具体实现。
package org.fenixsoft.classloading;public class ParentClassTest {    public static String parentStaticField = "父类静态字段";    static {        System.out.println("初始化父类。。。");    }
package org.fenixsoft.classloading;public class ChildClassTest extends ParentClassTest{    public static String childStaticField = "子类静态字段";    static {        System.out.println("初始化子类");    }

验证:

package org.fenixsoft.classloading;/** * 被动引用实例一: * 通过子类引用父类的静态字段,不会导致子类初始化 * */public class PassiveReferenceTest {    public static void main(String[] args) {        System.out.println("输出:" + ChildClassTest.parentStaticField);    }}

执行结果:

初始化父类。。。输出:父类静态字段

这里写图片描述
对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数来观察到此操作会导致子类的加载,子类没有初始化
这里写图片描述
执行结果:

......[Loaded java.security.BasicPermissionCollection from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded org.fenixsoft.classloading.PassiveReferenceTest from file:/D:/eclipse/workspace/JVMTest/build/classes/][Loaded sun.launcher.LauncherHelper$FXHelper from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded org.fenixsoft.classloading.ParentClassTest from file:/D:/eclipse/workspace/JVMTest/build/classes/][Loaded org.fenixsoft.classloading.ChildClassTest from file:/D:/eclipse/workspace/JVMTest/build/classes/]初始化父类。。。输出:父类静态字段[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]

这里写图片描述

3.3.4.2 通过数组定义来引用类,不会触发此类的初始化

package org.fenixsoft.classloading;/** * 被动引用实例2: * 通过数组定义来引用类,不会触发此类的初始化 * */public class PassiveReferenceTest2 {    public static void main(String[] args) {        ChildClassTest[] arr = new ChildClassTest[10];        System.out.println(arr);    }}

执行结果:

...[Loaded org.fenixsoft.classloading.PassiveReferenceTest2 from file:/D:/eclipse/workspace/JVMTest/build/classes/][Loaded sun.launcher.LauncherHelper$FXHelper from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded org.fenixsoft.classloading.ParentClassTest from file:/D:/eclipse/workspace/JVMTest/build/classes/][Loaded org.fenixsoft.classloading.ChildClassTest from file:/D:/eclipse/workspace/JVMTest/build/classes/][Lorg.fenixsoft.classloading.ChildClassTest;@2a139a55[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]

这里写图片描述

  • 触发了一个由虚拟机自动生成、直接继承于java.lang.Object的子类[Lorg.fenixsoft.classloading.ChildClassTest,数组创建由字节码指令newarray触发。
  • [Lorg.fenixsoft.classloading.ChildClassTest这个类代表了一个元素类型为org.fenixsoft.classloading.ChildClassTest的一维数组,数组对应的属性(length)和方法都定义在此类中

3.3.4.3 final常量不会触发初始化

package org.fenixsoft.classloading;/** * getstatic : 获取一个类的静态字段 * 被final关键字修饰的静态字段,不需要进行初始化,因为在编译期已经存入了常量池 * */public class GetFinalStaticTest {    public final static String staticField = "getstatic";    static {        System.out.println("final修饰的字段会在编译期就放入了常量池,不需要进行初始化");    }}
package org.fenixsoft.classloading;/** * 被动引用实例3: * final修饰的常量在编译阶段会存入调用类的常量池中,本质上并没有引用到定义常量的类, * 因此不会触发定义常量的类的初始化 * */public class PassiveReferenceTest3 {    public static void main(String[] args) {        System.out.println(GetFinalStaticTest.staticField);    }}

执行结果:

...[Loaded java.security.BasicPermissionCollection from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded org.fenixsoft.classloading.PassiveReferenceTest3 from file:/D:/eclipse/workspace/JVMTest/build/classes/][Loaded sun.launcher.LauncherHelper$FXHelper from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]getstatic[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar][Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar]

这里写图片描述

  • 没有输出“final修饰的字段会在编译期就放入了常量池,不需要进行初始化”,说明 org.fenixsoft.classloading.GetFinalStaticTest类没有初始化,可以看出也没有加载。
  • 这是因为在编译阶段通过常量传播优化,已经将此常量值“getstatic”存储到PassiveReferenceTest3 类的常量池中,以后PassiveReferenceTest3 类对常量GetFinalStaticTest.staticField的引用实际都转化为PassiveReferenceTest3 类对自身常量池的引用。
  • 也就是说,PassiveReferenceTest3 类的Class文件之中并没有GetFinalStaticTest类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系了。

3.4 接口的初始化

接口的加载过程和类加载过程稍有不同:

  • 类初始化代码可以通过静态语句块”static{}”来输出初始化信息
  • 接口中不能使用静态语句块,但编译器仍然会为接口生成”()”类构造器,用于初始化接口中定义的成员变量

接口与类真正的区别:

  • 在前面的5种有且只有需要必须立即初始化场景中的第三种 - 当一个类在初始化时,要求其父类全部都已经初始化过了
  • 但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正使用到父接口时(引用接口中定义的常量)才会初始化。
原创粉丝点击