JVM笔记(一)类加载
类加载的时机
一个类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载这七个部分。其中的验证、准备、解析这三个部分又可以称为连接。
其中加载、验证、准备、初始化和卸载这5个步骤是按部就班去完成的,而解析这个阶段,则不一定:它可以在某些情况下在初始化之后再进行解析,这是为了支持Java语言的动态绑定而设计的。
当遇到下面的几种情形时,必须对类进行初始化:(此时加载、验证、准备这几个阶段必须在初始化之前开始)
1)当遇到new、putstatic、getstatic、invokestatic这四条指令时,如果一个类还没有初始化,则必须要触发其初始化。
用到这四条指令的常见场景:用new关键字实例化一个对象、读取或设置一个类的静态变量(用final修饰的除外)、调用一个类的静态方法
2)使用java.lang.reflect包里面的方法对类进行反射调用的时候,如果该类还没初始化,则需要先触发其初始化
3)当需要初始化一个类的时候,如果其直接父类还没初始化,则需要先触发其直接父类的初始化(所以Object类永远是最先初始化的一个类)
4)当虚拟机启动的时候,用户需要指定一个类作为执行的主类(包含main方法的类),虚拟机会先初始化这个主类
5)当使用JDK 1.7的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且此方法句柄对应的类并没有进行初始化,则需要触发其初始化。
以上这5种情形,都可以称之为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
被动引用例子之一
e.g:
public class SuperClass { static { System.out.println("This is super class"); } public static int value = 12; } public class SubClass extends SuperClass{ static { System.out.println("This is sub class"); } } public class Main { public static void main(String[] args) { System.out.println(SubClass.value); } }
以上代码执行时,结果为:
可以看到,其中并没有打印出SubClass里面静态代码块的内容,证明SubClass并未被初始化。因为这个静态变量是在SuperClass中定义的,通过子类来引用父类定义的静态变量,只会初始化父类,并不会初始化子类。
当需要初始化子类时,可以把以上代码稍作改动:
e.g:
public class Main { public static void main(String[] args) { System.out.println(new SubClass().value); } }
SubClass类与SuperClass类的代码并没有任何修改,只是在main方法里对SuperClass的静态字段换了一种调用方式,输出结果如下:
因为在main方法中,对SubClass使用了new关键字来实例化一个匿名对象,所以此时SubClass是主动引用,因此SubClass会被初始化。
被动引用之二:
public class SuperClass { static { System.out.println("This is super class"); } public static final int value = 12; } public class SubClass extends SuperClass{ static { System.out.println("This is sub class"); } } public class Main { public static void main(String[] args) { System.out.println(SubClass.value); } }
此时,输出结果为:
可以看到,除了输出用final修饰的静态变量,其他静态代码块里面的内容是没有输出的,证明这两个类此时并没有进行初始化。
这是因为,在编译阶段,通过常量传播优化,已经将此常量的值“12”存储到了主类的常量池之中,以后每一次在主类对此常量进行引用,实际上都被转化为对主类自身常量池的引用了。即是说,在编译阶段完成之后,主类中并没有对SuperClass类的符号引用入口,实际上,在两个类编译成Class之后,它们之间已经没有了联系。
被动引用之三:
public class SuperClass { static { System.out.println("This is super class"); } public static int value = 12; } public class SubClass extends SuperClass{ static { System.out.println("This is sub class"); } } public class Main { public static void main(String[] args) { SuperClass[] sc = new SuperClass[10]; } }
执行结果如下:
程序中并没有输出静态代码块中的内容,也没有输出静态字段,说明并没有触发类的初始化阶段(具体原因我似懂非懂,还不能做出合理的解释)。
类加载的过程
类的加载包括加载、验证、准备、解析、初始化这5个步骤
在加载阶段,虚拟机需要完成这三件事:
1)通过一个类的全限定名获取此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类各种数据的访问入口
验证:
1)文件格式验证:字节流是否符合Class文件的格式规范,并且能被当前的虚拟机处理,该验证阶段主要是保证字节流能正确地解析并存储于方法区之间;
2)元数据验证:对字节码描述的语义进行分析,以保证其描述的信息符合Java的语义规范。此验证阶段的目的主要是对类的元数据信息进行语义校验,保证不存在不符合Java语义规范的元数据信息;
3)字节码验证:主要目的是通过数据量和控制流分析,确保程序语义是合法的、符合逻辑的
4)符号引用验证:主要目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。
准备:
这个阶段是为类变量分配内存并设置初始值。类变量是存储在方法区之中的,而不是想普通的成员变量一样存储在java堆之中。准备阶段设置的初始值,通常都是零值或者null之类的值。
比如说,上述代码中的public static int value = 12;这一条语句,在准备阶段,value被设置的值只是0,到了初始化阶段才会被初始化为12.
当然这种是“通常情况”,当一个类变量带有ConstantValue属性(即用final修饰时),那么该类变量在准备阶段就会初始化为ConstantValue所指定的值
解析:
此阶段是将常量池中的符号引用替换为直接引用。
其中,符号引用是指,用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,所引用的目标也并不一定已经加载到内存之中。
而直接引用是指,能够直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,所引用的目标已经加载到内存之中。
初始化:
初始化阶段是执行类构造器<clinit>()的过程,在此阶段,类变量的值会被初始化成指定的值而非默认值。