Java虚拟机的类加载机制

Java程序的运行时需要JVM支持的,而在JVM的运行过程中,需要把所有的Java类加载到虚拟机中才能运行。本文介绍了JVM的类加载机制。

类的生命周期与加载时机

类的生命周期

一个类从被加载到虚拟机内存中开始,到被卸载出内存为止,整个生命周期包括了 加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中 验证、准备、解析 3部分统称为链接,如下图:

整个顺序并不是完全固定的,其中解析阶段可以在初始化之后再开始,这样便可以实现Java的运行时绑定(动态绑定)机制。

类的加载时机

JVM虚拟机规范并没有对类的加载时机做出严格的要求,只规定了以下五种情况需要立刻触发类的初始化:

其余条件下,可以由JVM虚拟机自行决定何时去加载一个类。

    • 遇到new,getstatic,putstatic和invokestatic这四个字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
    • 使用反射机制对类进行调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 当初始化一个类时,如果其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法),此时会先初始化这个类
    • 使用JDK1.7的动态语言支持时,如果一个MethodHandle实例最后的解析结果包含REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且这个方法句柄对应的类没有初始化,则需要先对其进行初始化。

主动引用和被动引用

上面五种条件也被称为对类的主动引用,除此之外其他引用类的方式都不会触发初始化,即类的被动引用,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class Father {
static {
System.out.println("father init.");
}
public static int val = 123;
}

public class Son extends Father {
static {
System.out.println("son init.");
}
}

当我们访问Son.val时,会发现并没有输出son init.

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类的静态字段,子类相当于是被动引用,也就不会被初始化了。

类的加载过程

下面简单的介绍一下整个加载过程中,每个阶段JVM都执行了什么操作:

加载(Loading)

加载过程是Java的一大特点,类的来源可以多种多样,压缩包、网络字节流、运行时动态计算生成(reflect)等等…这也造就了Java语言强大的动态特性。

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

这一过程主要是为了确保Class的字节流中包含的信息符合虚拟机标准,以免造成破坏

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证,通过数据流和控制流分析确定程序的语义是合法的
  4. 符号引用验证,确保解析动作能够正常执行
准备(Preparation)

这一阶段将会为类变量分配内存并设置其初始值,注意此时进行内存分配的仅包括类变量(static修饰),并且初始值通常情况下是数据类型的零值而不是设定值,如下例

1
public static int val = 123;

在这一阶段变量val的赋值是0而不是123,因为此时尚未执行任何Java方法,而对val复制的putstatic指令在初始化阶段后才会执行。当然也有特殊情况,如下

1
public static final int val = 123;

加上final关键字修饰后,Java编译时会为val生成ConstantValue属性,这时准备阶段就会根据设置将其值设置为123。

解析(Resolution)

此阶段虚拟机将常量池内的符号替换为直接引用,主要包含以下动作:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
初始化(Initialization)

这时类加载过程的最后一步,这部分开始真正的执行Java代码,也就是说,这个阶段可以由程序员参与。

此阶段其实就是执行类构造器<clinit>()方法的过程。

类加载器

类加载器(Class Loader)是Java虚拟机的一大创举,它将“获取类的二进制字节流”这个过程交给了开发人员自己去实现,只要编写不同的Class Loader,应用程序本身就可以用相应的方式来获取自己需要的类。

类与加载器的关系

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性。

通俗的讲,就是即便同一个Class文件,被不同的类加载器加载之后,得到也不是同一个“类”(equals方法返回false)。

双亲委派模型

从虚拟机角度讲,只有两种类加载器,一种是启动类加载器(Bootstrap ClassLoader),在hotpot上使用C++实现,属于虚拟机的一部分;另一种则是所有其他类的加载器,这些加载器是独立于虚拟机的,由Java语言实现的,从开发者角度看,可以分为以下两类:

  1. 扩展类加载器(Extension ClassLoader)
  2. 应用程序类加载器(Appliaction ClassLoader)

当然开发人员也可以自己编写类加载器,最终不同的类加载器之间的层次关系如下图所示:

这就是Java中著名的双亲委派模型,它要求除了顶级的BootStrap加载器之外,其他类加载器都必须有父类加载器,工作流程如下:

如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会自己去尝试加载这个类。

这样做的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。举个例子,比如java.lang.Object这个类,无论哪个类加载器加载时,最终都会委派给Bootstrap加载器去加载,这就保证了整个系统运行过程中的Object都是同一个类。

否则,如果用户自己编写了一个java.lang.Object类,并放在程序的classpath中,最终系统将会出现多个不同的Object类,整个Java体系就变得一团混乱了。

看官可在此打赏