学习 JVM 的第 n-2 天,了解了类加载机制,以及初始化主动引用及被动引用的各类状况,在此记录分享。
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这个过程被称做虚拟机的类加载机制。java
上面这段话是在周志明大佬的《深刻理解Java虚拟机》中的,做为类加载机制的概念在此摘录。
当咱们在编译器中选择运行下面这个Hello World
程序,从点击运行到程序中止运行会通过一系列复杂的过程,这些关于该类的过程就是类的生命周期。git
public class HelloWorld { public static void main(String[] args){ System.out.println("Hello World"); } }
类的生命周期分为加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备、解析三个阶段被统称为链接。数组
下面,就针对各个阶段作一个简单的介绍。安全
在加载阶段,虚拟机会经过类的全限定名查找并加载类的二进制字节流到方法区中。dom
咱们能够经过在启动时添加 JVM 参数-XX:+TraceClassLoading
,打开打印类的加载顺序功能。函数
在验证阶段,虚拟机须要确保被加载类的正确性,符合虚拟机规范,以及不会危害虚拟机自身的安全。学习
在此阶段中又有四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。具体可参考《深刻理解Java虚拟机(第三版)》7.3.2节。测试
在准备阶段,虚拟机会为类的静态变量分配内存,并将其初始化为默认值。spa
若是类字段的字段属性表中存在ConstantValue
属性的基本类型或字符串(即被final修饰),那在准备阶段变量值就会被初始化为初始值而非默认值。命令行
假设上文的HelloWorld
类中有两个类变量定义以下,那么在准备阶段这两个变量的值分别是 a=0,b=2。
public class HelloWorld { private static int a = 1; private static final int b = 2; }
在解析阶段,虚拟机会将常量池内的符号引用替换为直接引用。若是符号引用指向一个未被加载的类,那么解析阶段将触发此类的加载。
在初始化阶段,虚拟机会为类的静态变量赋予正确的初始值,这些赋值操做以及静态代码块中的代码会被编译器统一置于一个<Clinit>
方法中,这个方法仅会被执行一次。因此咱们能够根据类的静态代码块是否执行来判断一个类是否进行了初始化。
在 Java 虚拟机规范中规定了多种触发初始化的状况,被称为对类的主动引用。
在类的字节码中遇到new、getstatic、putstatic、invokestatic
这四条指令时,会触发类的初始化。
对于上述的触发初始化的主动引用状况有一些例外的状况:
对于静态字段,只有直接定义这个字段的类才会被初始化,所以经过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
经过数组定义来引用类,不会触发此类的初始化。由于数组由 Java 虚拟机直接生成的,经过下面的例子来讲明这一状况。
在HelloWorld
类中定义一个主方法,并在主方法中定义两个数组变量以下:
public class HelloWorld { public static void main(String[] args){ int[] a = new int[10]; MyObject[] b = new MyObject[10]; } } class MyObject { static { System.out.println("MyObject 类初始化..."); } }
而后对字节码文件进行反编译,在命令行中输入javap -c HelloWorld
,获得反编译的字节码指令以下:
➜ classes git:(master) ✗ javap -c HelloWorld Compiled from "HelloWorld.java" public class HelloWorld { public HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: newarray int 4: astore_1 5: bipush 10 7: anewarray #2 // class MyObject 10: astore_2 11: return }
能够看到,对于原始类型的数组变量,在字节码中经过指令newarray
完成建立;对于引用类型的数组变量,在字节码中经过指令anewarray
完成建立。
而对于引用类型的MyObject
类,anewarray
这条指令与上文中叙述的几种主动引用状况不符合,不知足初始化的条件,这与上述测试代码中没有执行MyObject
类的静态代码块的状况相符,即没有触发MyObject
类的初始化。
当一个常量字段的值在编译期不肯定时(如UUID.random().toString()
),那么它不会被放到调用类的常量池中。所以即使这个静态字段是一个常量(被final关键字修饰),但因为它在编译期是不肯定的,因此在程序运行时仍是会主动使用这个常量所在的类,从而触发常量所在类的初始化。
一个接口在初始化时,并不要求其父接口所有都完成了初始化,只有在真正使用到父接口的时候(如引用接口中运行期才肯定的常量)才会初始化。
因为接口是没法定义静态代码块的,因此没法像类那样经过类静态代码块的执行与否判断是否发生初始化。可是在接口的初始化过程当中,编译器一样会为接口生成<clinit>()
构造器,用于初始化接口中所定义的成员变量。
在初始化阶段,虚拟机会为类的静态变量赋予正确的初始值,固然接口类也会听从这条规定。因此咱们能够经过初始化阶段对静态字段的赋值来观察接口类是否进行了初始化,下面是验证的过程。
在父类接口中定义一个int parentA = 1/0;
,而后经过子类访问父类的parentRand
常量,根据上述第三条的结论,编译期没法肯定的常量parentRand
不会被放入调用类InterfaceDemo
的常量池,那么必然会触发子接口或父接口其中至少一个接口类的初始化(假设咱们不知道结论),若是触发父接口的初始化,那么会将1/0的值赋值给parentA
,当虚拟机计算1/0时,会抛出java.lang.ArithmeticException: / by zero
异常;若是只触发子接口的初始化,则不会抛出异常。
public class InterfaceDemo { public static void main(String[] args) { System.out.println(ChildInterface.parentRand); } } interface ParentInterface { int parentA = 1/0; String parentRand = UUID.randomUUID().toString(); } interface ChildInterface extends ParentInterface { String childRand = UUID.randomUUID().toString(); }
运行InterfaceDemo
类,输出以下,其中InterfaceDemo.java:15
第15行正是parentA
定义的位置,从而能够得出结论:在子接口使用到父接口时会触发父接口的初始化。
Exception in thread "main" java.lang.ExceptionInInitializerError at InterfaceDemo.main(InterfaceDemo.java:10) Caused by: java.lang.ArithmeticException: / by zero at ParentInterface.<clinit>(InterfaceDemo.java:15) ... 1 more
而后将main
函数中的输出改成ChildInterface.childRand
,运行的结果时输出了一个UUID
,没有抛出除零异常,从而得出结论:若子接口不使用父接口,不会触发父接口的初始化。
综上所述,验证了第4条结论——一个接口在初始化时,并不要求其父接口所有都完成了初始化,只有在真正使用到父接口的时候(如引用接口中运行期才肯定的常量)才会初始化。