把类的描述文件从class文件中加载到内存,并且完成数据的校验、转化解析和初始化,最终形成虚拟机可以直接使用的JAVA类型的过程叫类的加载。

一个类从加载到虚拟机内存中开始再到从内存中卸载为止,它一共会经历以下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中『验证、准备、解析』三个部分统称为『连接』。

类加载时机

《JAVA虚拟机规范》中并没有严格规定类的『加载』阶段何时开始(这点可以由虚拟机自己决定),但是严格规定了有且只有以下六种情况的时候,必须对类完成初始化(当然,加载、验证、准备需要在初始化前完成)。

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果发现类没有完成初始化,则需要首先触发其初始化阶段。
    下面几种情况会使用到上面四条字节码指令:
    • 使用new关键字实例化对象的时候
    • 读取或设置一个对象的静态字段的时候。(被final修饰的,已经在编译期被放入静态常量池的除外)
    • 调用一个类的静态方法的时候
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果发现类没有进行初始化,则需要首先触发其初始化
  • 当初始化一个类的时候,发现其父类还没有完成初始化,则需要先完成父类的初始化
  • 当虚拟机启动时,用户指定了主类(包含了main()的类),虚拟机会首先初始化这个类
  • 当时用JAVA7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  • 当一个接口定义了JAVA8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在值钱被初始化。

类加载阶段

加载

加载阶段的主要工作就是获取到类的二进制字节流,这个阶段具体做了三个操作:

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

验证

验证阶段主要的工作是验证输入的二进制流是否符合JAVA虚拟机的规范,虽然JAVA是一门安全的语言(无法通过JAVA代码访问数组以外的元素、将一个对象转换成他未实现的类型),但是在字节码层面上至少是可以模拟这些不安全的语义的,因此虚拟机在执行前检查这些输入的字节流就非常有必要了,这其实是在保护虚拟机不受恶意代码的攻击,保证系统的稳定性。

准备

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置初始值(注意这里是设置初始值是指设置各个类型的0值,并不是赋值,比如int a = 123,在这个阶段a = 0)的阶段,类的实例变量是随着类的实例化之后,一起在堆中分配的。在JDK7及以前,HotSpot虚拟机是用永久代来实现方法区的,但是JDK8开始类变量和实例对象一起放在了堆里,这时在说类变量放在方法区就不合适了。

解析

解析阶段的工作是将常量池中的符号引用转成直接引用。

初始化

初始化阶段其实就是为类变量赋值的一个阶段。在准备阶段,类变量已经由虚拟机完成了一次赋零值的过程,在初始化阶段则会依据程序编写的代码主观的对类变量完成赋值和其他资源的初始化。从代码的角度来看,初始化阶段其实就是执行类的构造器方法(clinit())的过程,clinit()是由javac自动生成的,它是由类变量赋值语句和静态语句块(被static{}包围的语句)中的语句合并而来。

下面有一段代码比较有意思,大家可以先看看,猜一下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Parent{
static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent{
static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B);
}

在main函数中的Sub.B会触发Sub类的初始化,在初始化Sub类的时候,发现其继承自Parent类,因此会先初始化Parent类。在初始化Parent类的时候,A的值的变化是这样的0 -> 1 -> 2,在准备阶段,A被赋零值,在初始化阶段,clinit()方法会依次给A赋值为1和2,所以最终A的值就是2。

双亲委派机制

双亲委派是JAVA类加载器的一种架构,所以在讲双亲委派的时候我们先了解一下JAVA的类加载器。

从虚拟机的角度来看,类加载器分为两种:启动类加载器(bootstrap Class Loader)和其他类型的加载器。其中启动类加载器是由C++编写,其他的加载器是由JAVA语言编写。

在开发人员的角度来看,JAVA类加载器一直保持着三层结构、双亲委派架构。

  • 启动类加载器

Bootstrap Class Loader是顶层的类加载器,它负责加载存放在$JAVA_HOME/lib目录下并且可以被虚拟机识别的类库(这里的加载指的是按固定名字加载比如rt.jar、tools.jar等,也就是说并不是随便放一个jar到$JAVA_HOME/lib就可以被加载的)。

  • 扩展类加载器

Extension Class Loader是以JAVA代码的形式出现的,主要负责加载$JAVA_HOME/lib/ext目录中的所有类库。

  • 应用程序类加载器

Application Class Loader是ClassLoader类中getSystem.getClassLoader()方法的返回值,所以有些时候也称为系统类加载器,主要负责加载ClassPath目录下的所有类库,开发者同样可以使用这个类加载器。在没有其他用户自定义类加载器的情况下,应用程序类加载器就是默认的类加载器。

双亲委派的工作原理:当一个类加载器收到一个加载类的请求时,首先不会尝试自己去加载这个类,而是把这个请求委托给父类去完成,每层类加载器都是如此,因此所有的类加载请求都会最终被传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试完成加载。

为什么要有这样的一个机制呢?

双亲委派这样的机制其实包含了一种层级结构(优先级),这样可以保证系统需要的类库优先加载。试想一下,加入自己编写了一个java.lang.Object类,放在了ClassPath路径下,那么程序运行时这样就出现了两个不同的Object类,这不就动摇了JAVA的根本了吗?更有甚者,往Object中添加一些恶意代码呢?所以使用双亲委派机制在一定程度上,保证了JVM的安全性。

需要特别说明的是,双亲委派模型中的父子关系并不是通过继承实现的,而是通过组合实现的