Java虚拟机类加载机制

整个生命周期(7个阶段):

1
加载->(验证->准备->解析)[连接]->初始化->使用->卸载

其中,加载、验证、准备、初始化和卸载这五步的先后顺序不可颠倒。
而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定(动态绑定)。

类加载的过程

加载->(验证->准备->解析)[连接]->初始化

加载:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。(并未限制从哪里获取以及如何获取)
    • zip,jar,ear,war
    • 网络中获取,Applet
    • 运行时计算生成,动态代理技术(java.lang.reflect.Proxy)
    • 其它文件生成(jsp)
    • ……
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

验证(4个阶段):

  1. 文件格式验证:(基于字节流)
    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理(是否以魔数0xCAFEBABE开头等)
  2. 元数据验证:(基于方法区的存储结构)
    对字节码的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(该类是否有父类等)
  3. 字节码验证[最复杂]:(基于方法区的存储结构)
    进行数据流和控制流分析。对类的方法进行校验分析。
  4. 符号引用验证:(基于方法区的存储结构)
    对类自身以外(常量池中的各种符号引用)的信息进行匹配性验证(通过字符串描述的全限定名能否找到对应的类)

准备

  • 正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
  • 分配的是类变量,不是实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中。

解析:是虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用:与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:与虚拟机实现的内存布局相关,如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对以下这四种引用的解析 :

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

初始化

到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说字节码)。

对于初始化阶段,虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行”初始化”(而加载、验证、准备自然要在此之前开始)

  1. 遇到new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化操作。生成这4条指令的最常见的Java代码场景是:

    • 使用new关键字实例化对象的时候
    • 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 调用一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化一个类时,发现其父类还没有被初始化,则需要先父类初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

类的加载-双亲委派模型

对于任意一个类,都要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。不同类加载器加载的同一个Class文件,那这两个类就必定不相等。

双亲委派模型:

  • 启动类加载器(Bootstrap ClassLoader),由C++语言实现,是虚拟机自身的一部分。
    负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动器加载器无法被Java程序直接引用。

  • 其它类加载器,由Java语言实现,独立与虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader

    1. 扩展类加载器(Extension ClassLoader):负责加载 <JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
    2. 应用程序类加载器(Application ClassLoader):由于这个类是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的加载器。
      1
      2
      3
      4
      5
      6
      7
      		启动类加载器
      |
      扩展类加载器
      |
      应用程序类加载器
      | |
      自定义类加载器 自定义类加载器

类加载器中的这种层次关系,就称为类加载器的双亲委派模型,除了启动类加载器外,所有的类加载器都应有自己的父类加载器,一般不会以继承关系实现,而是使用组合关系来复用父加载器的代码。

双亲委派模型工作过程:
任何一个类加载器(启动类加载器除外)收到加载类的请求,它首先把这个请求发给父类,若父类无法加载,子加载器才会尝试自己去加载。
例如类java.lang.Object 放在 rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
双亲委派模型的实现非常简单,在java.lang.ClassLoader的loadClass()方法中,

类的加载顺序:1. 先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空,则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。