Java虚拟机系列之类加载机制

174 阅读4分钟

类加载时机

类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。加载可以有实现方式,比如本地加载类、或者通过网络加载类,具体实现自由。对于初始化阶段,虚拟机规范规定有且只有5中情况,如果没有初始化,则必须对类进行初始化:

  • 遇到new,getstatic,putstat或invokestatic指令时
  • 使用反射进行调用时
  • 当初始化类时,其父类没有初始化,则需触发父类初始化
  • 用户指定了一个主类,vm会先初始化这个主类
  • 一个MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄, 并且这个方法句柄对应的类没有初始化时

类加载过程

加载过程包括:加载,验证,准备,解析和初始化

加载

加载阶段要做的就是

  • 通过一个类的全限定名来获取类的二进制字节流;
  • 将这个字节流转化为方法区的运行时数据结构;
  • 生成这个类的Class对象,作为方法区的这个类的各种数据的入口

验证

验证是保证Class文件的字节流中包含的信息符合虚拟机的规定,包括

  • 文件格式验证,验证字节流是否符合Class文件格式的规范,如是否以魔数0XCAFEBABE开头
  • 元数据验证,对字节码描述的信息进行语义分析,如 这个类是否继承了final描述的类
  • 字节码验证,通过数据流和控制流分析,确定程序语义是合法的,如方法体内的类型转化是否有效

准备

准备阶段是正式为类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里仅包括类变量,而不是实例变量。如 public static int val = 123;在准备阶段 val的初始值为0,而不是123。

解析

解析阶段是虚拟机讲常量池内的符号引用替换为直接引用。符号引用是用一组符号描述所引用的目标对象,跟内存布局无关;而直接引用可以是指向目标对象的指针、相对偏移量或者一个间接定位的目标的句柄,跟内存布局有关。

解析包括:类或接口的解析、字段解析、类方法解析、接口方法解析等

初始化

初始化阶段是真正执行类中定义的Java字节码,这个阶段是执行类构造器 clinit()方法初始化类变量的阶段,但clinit()并非必需,如果没有静态代码块或者类变量,就可以没有clinit()

双亲委派模型

类加载器

Java虚拟机有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个是使用C++实现的,是虚拟机自身的一部分;另一种是所有其他的类加载器,由Java语言实现,继承自java.lang.ClassLoader。

在Java程序中,比较两个类是否相等,只有在这两个类是有同一个类加载器加载的前提下才有意义。

  • 启动类加载器:负责加载JAVA_HOME\lib目录下的,或被-Xbootclasspath参数所指定的类库
  • 扩展类加载器:负责加载JAVA_HOME\lib\ext目录中,或被java.ext.dirs系统变量所指定的类库
  • 应用程序类加载器:负责加载用户类路径上所指定的类库

双亲委派模型

如图展示的类加载器之前的这种层次关系,成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器必须有自己的父类加载器。

双亲委派模型要求:如果一个类加载器收到了类加载的请求,它首先把请求委派给父类加载器,每个层次的加载器都是如此,只有当父类加载器反馈无法完成这个加载请求,子加载器才会尝试自己去加载。

双亲委派模型并非一个强制性的约束模型,有3次规模是破坏双亲委派模型的

  • 引入双亲委派模型时,为了兼容ClassLoader原有的loadClass方法
  • 模型自身的缺陷:此模型很好的解决了各个类加载器的基础类的统一问题,越基础的类由越上层的加载器加载,但也有基础类调回用户代码的情况,如JNDI服务,为了解决这个问题,Java语言提供了线程上下文加载器,此加载器可以通过setContextClassLoader进行设置,作为应用程序的加载器
  • 用户对程序动态性的追求:如代码热替换