加载完成后要开始对那些字节流进行检验了(其实很多步骤是跟上面交叉进行的,比如文件格式验证):

  检验的目的:确保class文件的字节流信息符合jvm的口味,不会让jvm感到不舒服。假如class文件是由纯粹的java代码编译过来的,自然不会出现类似于数组越界、跳转到不存在的代码块等不健康的问题,因为一旦出现这种现象,编译器会拒绝编译了。但是,跟之前说的一样,Class文件流不一定是从java源码编译过来的,也可能是从网络或者其他地方过来的,甚至你可以自己用16进制写,假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm完全崩溃。

  检验主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证

  文件格式验证:验证字节流是否符合Class文件格式的规范并 验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。

  元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。

  字节码检验:复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。

  符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)

  验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。

  接着上面步骤完成后,会进入准备阶段了:

  这阶段会为类变量(指那些静态变量)分配内存并设置类比那辆初始值的阶段,这些内存在方法区中进行分配。这里要说明一下,这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。这里的给类变量设初始值跟类变量的赋值有点不同,比如下面:

public static int value=123;

  在这一阶段,value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于<clinit>(),所以,给value赋值为123是在初始化的时候才会执行的。

  这里也有个例外:

public static final int value=123;

  这里在准备阶段value的值会初始化为123了。这个是说,在编译期,javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jm会根据这个ConstantValue的值来为value赋值了。

  完成上步后,要进行解析了。解析好像是对类的字段,方法等东西进行转换,具体涉及到Class文件的格式内容,并没深入去了解。

  初始化过程是类加载过程的后一步:

  在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其他的动作完全有jvm主导,到了初始化这块,才开始真正执行java里面的代码。

  这一步将会执行一些预操作,注意区分在准备阶段,已经为类变量执行过一次系统赋值了。

  其实说白了,这一步是执行程序的<clinit>();方法的过程。下面我们来研究一下<clinit>()方法:

  <clinit>()方法叫做类构造器方法,有编译器自动手机类中的所有类变量的赋值动作和静态语句块中的语句合并而成的,置于他们的顺序与在源文件中排列的一样。

  <clinit>();方法与类构造方法不一样,他不需要显示得调用父类的<clinit>();方法,虚拟机会保证子类的<clinit>();方法在执行前父类的这个方法已经执行完毕了,也是说,虚拟机中第一个被执行的<clinit>();方法肯定是java.lang.Object类的。

  下面来个例子说明一下:

    static class Parent{ 
        public static int A=1; 
        static{ 
            A=2; 
        } 
    } 
    static class Sub extends Parent{ 
        public static int B=A; 
    } 
    public static void main(String[] args){ 
        System.out.println(Sub.B); 
    }