Java语言的哲学:一切都是对象。对于Java虚拟机而言,一个普通的Java类一样是一个对象,那若是是对象,必然有它的初始化过程。一个类在JVM中被实例化成一个对象,须要经历三个过程:加载、连接和初始化。java
加载:从字节码二进制文件——.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,全部的程序必须加载到内存才能工做。一个Java类在被加载到内存后会在Java堆中建立一个类(java.lang.Class)对象,同时JVM为每一个类对象都维护一个常量池(相似于符号表)。c++
Java类都是由类加载器进行加载,从大的分类来看,Java提供两种类型的类加载器:和用户自定义的类加载器。Java默认提供了3个类加载器,分别是:Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。tomcat
除了以上3个类加载其以外,用户还能够继承ClassLoader类来自定义相应的类加载器。JVM经过一种双亲委托模型来避免重复加载同一个类,在这种模型中,当一个类C须要被某一个类加载器L加载时,会优先在类加载器L的父类中查找类C是否已经被加载。下面的代码是具体的双亲委托模式的实现:安全
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
全部的Java类加载器都保证是引导类加载器的孩子,具体的ClassLoader体系结构见下图:函数
连接:包含了验证和准备类或者接口、包括了它的直接父类、直接父接口、元素类型以及一些必要的操做。Java虚拟机规范并没明确要求被准备的类或接口须要被解析,只须要验证相关的类或接口的字节码符合JVM规范。this
类的初始化:执行类的static块和初始化类内部的静态属性。编码
一般用这两种方式来动态加载一个java类,可是两个方法之间也是有一些细微的差异。spa
查看Class类的具体实现可知,实质上这个方法是调用原生的方法:线程
private static native Class<?> forName0(String name, boolean initialize,ClassLoader loader);
形式上相似于Class.forName(name,true,currentLoader);设计
综上所述,Class.forName若是调用成功
若是采用这种方式的类加载策略,因为双亲托管模型的存在,最终都会将类的加载任务交付给Bootstrap ClassLoader进行加载。跟踪源代码,最终会调用原生方法:
private native Class<?> findBootstrapClass(String name);
与此同时,与上一种方式的最本质的不一样是,类不会被初始化。
总结ClassLoader.loadClass若是调用成功:
对于普通的Java程序,通常都不须要显式的声明来动态加载Java类,只须要用import关键字将相关联的类引入,类被第一次调用的时候,就会被加载初始化。那对于一个类对象,其内部各组成部分的初始化顺序又是如何的呢?
一个Java类对象在初始化的时候一定是按照必定顺序初始化其静态块、静态属性、类内部属性、构造方法。这里咱们讨论的初始化分别针对两个对象,一个是类自己还有一个是类实例化的对象。
类自己的初始化会在类被加载完毕、连接完成以后,由Java虚拟机负责调用<clinit>方法完成。在这个方法中依次完成了堆类内部静态块的调用和类内部静态属性的初始化(若是存在父类,父类会优先进行初始化)。不论建立多少个实例化的对象,一个类只会被初始化一次。
类实例化的对象经过new操做建立,Java虚拟机保证一个类在new操做实例化其对象以前已经完成了类的加载、连接和初始化。以后Java虚拟机会调用<init>方法完成类实例化对象的初始化。这个方法会优先按照代码中顺序完成对类内部个属性的初始化,以后再调用类的构造函数(若是有父类,则优先调用父类的构造函数)。
PS:须要注意的是上述提到的<init>和<clinit>方法都是非法的Java方法名,是由编译器命名的,并不能由编码实现。
综上所述,咱们大体能够得出如下结论,对于一个类,在实例化一个这个类的对象时,咱们能够保证如下这样的优先级进行初始化:
类内部静态块 > 类静态属性 > 类内部属性 > 类构造函数
最近看了几篇谈设计模型单例模式的Java实现的文章,在实现一个具体的线程安全单例Java类中,一个简单且被推荐的方式使用内部静态类存储一个静态属性,这就涉及到内部静态类的初始化顺序的问题,结合想到这篇文章中也没有讨论过这个问题,继而作了一些实验,代码以下:
public class Test { public static class Inner{ public final static Test testInstance = new Test(3); static { System.out.println("TestInner Static!"); } } public static Test getInstance(){ return Inner.testInstance; } public Test(int i ) { System.out.println("Test " + i +" Construct! "); } static { System.out.println("Test Stataic"); } public static Test testOut = new Test(1); public static void main(String args[]){ Test t = new Test(2); Test.getInstance(); } }
实验的结果证实顺序以下:
内部类静态属性(或静态块)会在内部类第一次被调用的时候按顺序被初始化(或执行);而类内部静态块的执行先于类内部静态属性的初始化,会发生在类被第一次加载初始化的时候;类内部属性的初始化先于构造函数会发生在一个类的对象被实例化的时候。
综合上面的结论,上面这段代码的结果是什么呢?问题就留给读者们自行思考吧。