Class Loader

概述

Class文件被加载到内存中需要以下几步

  1. 加载
  2. 验证 ——
  3. 准备 | 连接阶段
  4. 解析 ——
  5. 初始化
  6. 使用
  7. 卸载
  • 解析阶段可能在初始化之前也可能在初始化之后,为了支持Java的运行时绑定。
  • 加载、验证、准备一定要在初始化之前完成

类初始化的时机

虚拟机严格规定了有且只有5种情况必须立即对类进行初始化:

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令,最常见的就是new一个对象或者访问static变量
  2. 对类进行反射调用
  3. 初始化一个类时如果其父类未被初始化时
  4. 虚拟机启动时会初始化main方法的那个类
  5. 使用JDK1.7的动态语言支持时,如果一个java.lang.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法所对应的类没有进行过初始化。

类加载

主要有三步

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。(例如jar、zip文件,网络等等)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(方法区存储类信息、常量和静态变量)
  3. 在内存中生成一个代表这个类的java.lang.class对象。

验证

目的是为了确保Class文件的字节流符合虚拟机的要求,并且不会危害到虚拟机。

  1. 文件格式验证:主要是验证Class文件格式的规范,并且能被当前版本的虚拟机处理。例如是否魔数CAFEBABE开头,版本是否在范围内等等
  2. 元数据验证:对类的描述进行语义分析即语法。例如是否有父类,是否继承了不应该继承的类,类中的字段或方法是否会和父类发生冲突。
  3. 字节码验证:目的是为了确保程序语义是合法并合乎逻辑的。比如类型转换是否有效等等
  4. 符号引用验证:这个阶段校验是在解析阶段中发生,即将符号引用转化为直接引用。主要是对类自身以外的信息进行匹配性校验,例如访问域是否符合规范,根据全限定名是否能找到对应的类等等。这个阶段的验证目的主要是为了解析动作能正常执行。

准备

准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段。这些变量使用的内存都将在方法区中进行分配。这时候分配的还不包括实例变量,实例变量将在对象初始化之后分配在Java堆中,并且这里为类变量设置的初始值不是代码中的默认值而是虚拟机为它赋的初始值,例如有如下这个类变量

1
public static int value = 123;

value在准备阶段之后被赋值为0而不是123,要等到类构造器方法<clinit>调用的时候才会赋值为123。但是有种特殊情况:如果这个值用final修饰,那么在编译的时候就会生成一个ConstantValue(假设123),之后在准备阶段就会直接用ConstantValue(123)赋值。

解析

解析阶段主要是将符号引用转为直接引用。这两者之间的概念参考了这篇回答:https://www.zhihu.com/question/30300585

由于编译的时候不知道class文件会被加载到内存的哪一块地址,因此只能用符号来代替,例如A要调用test()方法(假设对应的符号是#6),但是不知道test()方法会存在哪儿,只能用A.#6来表述,而当加载到了内存之后就可以将#6替换为实际的内存地址,而解析阶段干的就是这个工作。

初始化

初始化阶段是执行类构造器<clinit>方法的过程,<clinit>是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并生成的。当然也是按照顺序收集的,因此如下代码会报错

1
2
3
4
5
6
7
public class Test {
static {
i = 0; //可以赋值
System.out.print(i); //不能访问,会提示 Illegal forward reference
}
static int i = 1;
}

对于class来说,子类的<clinit>方法执行之前一定会执行父类的,但对接口来说并不需要先执行父类的<clinit>方法,只有在使用的时候才会执行。

<clinit>方法对于类或接口来说不是必须的,如果没有静态语句块或者类变量的话,编译器可以不为这个class生成<clinit>方法。

类加载器

类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”被放到了JVM外部去实现,而实现这个模块的代码就是类加载器。

每一个类加载器都有一个独立的类名称空间,比较两个类是否相等,只有在两者是由同一个加载器加载的前提下才有意义,例如ClassLoader1和ClassLoader2都加载了HelloWorld.class,但其实两个HelloWorld.class并不同。

从JVM来看三种不同的类加载器

  • 启动类加载器(Bootstrap ClassLoader): 用C++实现,是虚拟机自身的一部分。负责加载<JAVA_HOME>\lib目录下的文件。比如 java.util.、java.io.、java.nio.*。
  • 扩展类加载器(Extension ClassLoader): 负责加载<JAVA_HOME>\lib\ext目录下的文件。比如 swing 系列、xml 解析器等等。
  • 应用程序类加载器(Application ClassLoader): 这个加载器是ClassLoader.getSystemClassLoader()的返回值,因此也叫系统类加载器。负责加载用户类路径下的文件。如果没有自定义,那么一般默认是使用这个类加载器。

双亲委派模型

工作原理是:如果一个类加载器收到了类加载的请求,他不会自己加载,而是委派给父类加载,并且每层如此,只有当父类加载器无法完成这个加载请求时,才会由子加载器尝试加载。

这个模型的好处一个是避免了重复加载的情况发生,保证同一个class被加载之后不会产生不同的结果,另一个是为了安全性,核心的类永远只会由启动类加载器加载。

关于破坏双亲委派模型的内容需要重新写一篇。