疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

java类加载的过程---java培训


 

  今天尝试一种加载过程,现在也总结:一个文件从装载卸载的生命历程,总要经历四个阶段:

  加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

  其中加载(除了自定义加载)+链接的过程是完全由jvm负责的,什么时候要对类进行初始化工作(加载+链接在此之前已经完成了),jvm有严格的规定(四种情况):

  1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,加入类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。

  2.使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。

  3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。

  4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。

  以上4种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。下面也举了些被动引用的例子:

  /**

  * 被动引用情景1

  * 通过子类引用父类的静态字段,不会导致子类的初始化

  * @author volador

  *

  */

  class SuperClass{

  static{

  System.out.println("super class init.");

  }

  public static int value=123;

  }

  class SubClass extends SuperClass{

  static{

  System.out.println("sub class init.");

  }

  }

  public class test{

  public static void main(String[]args){

  System.out.println(SubClass.value);

  }

  }

  输出结果是:super class init。

  /**

  * 被动引用情景2

  * 通过数组引用来引用类,不会触发此类的初始化

  * @author volador

  *

  */

  public class test{

  public static void main(String[] args){

  SuperClass s_list=new SuperClass[10];

  }

  }

  输出结果:没输出

  /**

  * 被动引用情景3

  * 常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始化

  * @author root

  *

  */

  class ConstClass{

  static{

  System.out.println("ConstClass init.");

  }

  public final static String value="hello";

  }

  public class test{

  public static void main(String[] args){

  System.out.println(ConstClass.value);

  }

  }

  输出结果:hello(tip:在编译的时候,ConstClass.value已经被转变成hello常量放进test类的常量池里面了)

  以上是根据类型的初始化,界面还初始化,初始化的接口和类的初始化有点不同:

  上面的代码都是使用静态{ }输出信息,接口不能做,但接口初始化时编译器会给接口生成()类构造函数初始化接口成员变量,这类初始化上也要做。真正不同的地方是第三点,该类的初始化之前实施要求父类的所有初始化完成,但似乎父亲接口初始化接口初始化别感冒了,也就是说,子接口初始化时不需要他的父亲接口完成初始化,只有在一个真正的父亲用接口将被初始化(如参考界面上的常数时间!)。

  下面分解一下一个类的加载全过程:加载->验证->准备->解析->初始化

  首先是加载:

  这一块虚拟机要完成3件事:

  1.通过一个类的全限定名来获取定义此类的二进制字节流。

  2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

  关于第一点,很灵活,很多技术都是在这里切入,因为它并没有限定二进制流从哪里来:

  从class文件来->一般的文件加载

  从zip包中来->加载jar中的类

  从网络中来->Applet

  ..........

  相比与加载过程的其他几个阶段,加载阶段可控性最强,因为类的加载器可以用系统的,也可以用自己写的,程序猿可以用自己的方式写加载器来控制字节流的获取。

  获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在java堆中实例化一个java.lang.Class对象与堆中的数据关联起来。

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

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

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

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

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

  字节代码检查:最复杂的方法,对检验的内容,保证运行时将不做任何平凡的事情。

  象征性的参考验证:验证真伪的参考和可行性,如代码内导致另一类,这是检查是否有或没有谁来存在;或代码访问其他类型的一些特性,这是因为这些属性可以接入线路测试。(这一步将身后的分析工作奠定基础)