类加载机制

定义

把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

类的生命周期

加载验证准备解析初始化使用卸载。其中验证,准备,解析3个部分统称为连接。
这7个阶段发生顺序如下图:

类加载步骤
类加载步骤

其中加载,验证,准备,解析及初始化是属于类加载机制中的步骤。注意此处的加载不等同于类加载。

触发类加载的条件

  1. 遇到new,getstatic,putstaticinvokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。生成这4条指令的最常见的Java代码场景是:
    • 使用new关键字实例化对象的时候
    • 读取或设置一个类的静态字段的时候(被final修饰,已在编译期把结果放入常量池的静态字段除外)
    • 调用一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先出发父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发初始化。

类加载的具体过程

加载:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区内的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证:
是连接阶段的第一步,目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。注意,这个校验比静态编译器将源码转换成字节码时的校验更为严格!
包含四个阶段的校验动作

  1. 文件格式验证
    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
  2. 元数据验证
    对类的元数据信息进行语义校验,是否不存在不符合Java语言规范的元数据信息
  3. 字节码验证
    最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
  4. 符号引用验证
    最后一个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三个阶段——解析阶段中发生。符号验证的目的是确保解析动作能正常进行。

准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中分配。只包括类变量。初始值“通常情况”下是数据类型的零值。
“特殊情况”下,如果类字段的字段属性表中存在ConstantValue 属性,那么在准备阶段变量的值就会被初始化为 ConstantValue 属性所指定的值。

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

1
public static int value = 123;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

1
public static final int value = 123;

解析:
虚拟机将常量池内的符号引用替换为直接引用的过程。
“动态解析”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。

初始化:
类加载过程中的最后一步。
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
<clinit>()与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
简单地说,初始化就是对类变量进行赋值及执行静态代码块

类加载器

前面提到的加载部分的功能是将类的class文件读入内存,并为之创建一个java.lang.Class对象。这部分功能就是由类加载器来实现的。

类加载分类

类加载器类似于原始部落结构,存在权力等级制度。类加载器具有等级制度,但并非是继承关系(毕竟老大是用c++写的。。)以组合的方式来复用父加载器的功能,这也符合组合优先原则。

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的(Object、System、String等)。
  • 平台类加载器(Platform ClassLoader):用以加载一些扩展的系统类。如 XML、加密、压缩相关功能类:
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,通过
    ClassLoader.getSystemClassLoader()方法直接获取。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
    以上3种类加载器基本上负责了所有Java类的加载。下面我们来具体了解上述几个类加载器实现类加载过程时相互配合协作的流程。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派模型
双亲委派模型

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个位置,要非常礼貌的向上逐级询问:“请问,这个类已经加载了吗?”被询问的高层次类加载器会自问两个问题:第一,我是否已经加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的回答均为“否”时,才能让当前类加载器加载这个未知类。如图所示,左侧绿色箭头向上主机询问是否能加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载。在右侧三个小标签里,列举了此层类加载器主要加载的代表性类库,事实上不止于此。

这样的好处是不同层次的类加载器具有不同优先级,比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。

双亲委派模型的代码实现

ClassLoader中loadClass方法实现了双亲委派模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果该类没有加载,则进入该分支
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
long t1 = System.nanoTime();
c = findClass(name); //用户可通过覆写该方法,来自定义类加载器
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}

整个流程大致如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。关于自定义类加载器,本篇文章就不介绍了,主要是重写findClass方法。