之前我们了解到,Class在java中经常以文件的形式存在,并且只有当被虚拟机装载的Class类型才能在程序中使用,系统装载类的过程可以划分为三个部分,加载,链接初始化。可以参考下图帮助理解,好有个大致的印象。
类的装载
像前面说的那样,java虚拟机并不会无条件的加载类,而是当一个类或者是接口在初次使用前就必须要进行初始化,这里的使用说的是“主动”使用,主动的情况有一下几种:
1.当创建一个类的实例的时候,比如说使用了new关键字,或者通过克隆,反射,或者是反序列化的操作。
2.当调用类的静态方法的时候,也就是说,我们使用的字节码invokestatic指令时。
3.当我们使用到类或者是接口中的静态字段时,不包含final所修饰的字段,回归到指令,体现为getstatic和putstatic的使用。
4.当我们使用java.lang.reflect包中的方法反射类的方法时,也会使得其主动加载类。
5.当初始化子类的时候,按照之前了解的内容,子类的初始化必须先初始化父类,所以,此时会加载相应的父类进行加载。
6.作为虚拟机启动的main()方法,如果此类包含main()方法,则这个类会加载到虚拟机中。
参考下面的例子来体会下:
父类:
public class Parent { static { System.out.println("Parent init"); } }
子类:
public class Child extends Parent{ static{ System.out.println("Child init"); } }
主函数类:
public class InitMain { public static void main(String[] args){ Child c = new Child(); } }
运行结果:
可以看到,符合我们的预期,同时证明了两个条件,条件一和条件五。那我们改在一下,来一个“被动”的例子:
修改下父类,子类不变:
public class Parent { static { System.out.println("Parent init"); } public static int v = 100; }
修改下主函数类:
public class InitMain { public static void main(String[] args){ System.out.println(Child.v); } }
执行结果:如下:
可以看到,这里是通过子类访问的父类,但是,这里解释为,Child类并没有被初始化,而只有Parent父类被初始化,如果我们使用-XX:+TraceClassLoading参数运行,得到以下日志:
可以看到,类都被加载了,但是只有父类被初始化了,子类并没有被初始化,但是需要强调的是,这两个类都已经被jvm加载,这个同时验证了第五条的情形。
再来举一个例子,验证一下final常量的引用,看一下有什么不同。
public class FinalFiledC { public static final String constString = "COnNST"; static{ System.out.println("FinalFieldC init"); } }
测试主函数类:
public class UseFinalField { public static void main(String[] args){ System.out.println(FinalFiledC.constString); } }
运行结果为:CoNST;
表面上看不出什么,那让我们打印一下其类的加载的日志:
可以看到,类根本不会被加载到系统中,而是直接接我们要使用的final字段量存放到常量池中,并没有引用FinalFieldC类,可以发现,在javac进行编译的时候,直接植入目标了类,不再使用引用类获取final修饰的字段常量。
其实加载类除以第一个阶段,加载类的时候Java虚拟机必须完成下面的工作:
通过类的全名获取二进制数据流,解析类的二进制数据流为方法区内的数据结构,并创建java.lang.Class类的实例标识该类型。二进制数据流来源较广,一般的可以读取一个后缀为.class的文件,或者也可以读入jar,zip等,也可以通过Http协议通过网络进行加载,这类似于之前的Jsp的使用,下载到本地的class文件再进行执行。
到这里,关于类的加载我们了解了其加载的条件,已经走了第一步,接下来会做什么呢,按照上面的流程图我们需要到链接部分了,下一节将讲述类在连接部分进行了哪些操作。