什么是虚拟机类的加载机制?

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

类从加载到虚拟机的内存中开始,直到卸载出内存为止,整个生命周期为:

  • 加载(loading)

  • 验证(verification)

  • 准备(preparation)

  • 解析(resolution)

  • 初始化(initialization)

  • 使用(using)

  • 卸载(unloading)

其中 验证、准备、解析部分统称为连接

接下来依次讲解,各个步骤所做的事

第一部分 加载


“加载”是“类加载”的一个阶段,注意区分概念。类的加载由类加载器(后面介绍)加载主要完成三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

加载阶段完成后,虚拟机外部的二进制字节流将按照虚拟机所需的格式存储在方法区中,同时在内存中实例化一个java.lang.Class的实例对象。相对于HotSpot,这个实例对象比较特殊,虽然是一个对象,但并没有放置在堆中,而是放置在方法区中。这个对象将作为程序访问方法区中这些类数据的外部接口。

第二部分 验证


这一步主要是确保Class文件的字节流符合虚拟机的规范

主要验证以下几个部分:

1、文件格式验证
验证是否以魔数开头、主次版本号是否在当前虚拟机处理范围内…

这一验证阶段主要是保证输入的字节流能正确地解析并存储与方法区内,格式上符合Java类型信息的要求。只有通过这个阶段,字节流才会进入内存的方法区中存储,后面的三个验证方式也都是给予方法区中的数据验证,不再会操作字节流。

2、元数据验证 验证这个类是否有父类、这个类是否继承了不允许继承的类…

该阶段主要对类的元数据进行语义校验,保证符合java语言规范的元数据信息。

3、字节码验证

最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。

4、符号引用验证

这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在连接的解析阶段。目的是确保解析动作正常执行,如果无法通过验证,将抛出 IllegalAccessError、NoSuchFieldError、NoSuchMethodError等异常。

第三部分 准备


准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所实用的内存将在方法区中进行分配。

这时候的分配仅仅是类变量(staic修饰的变量),而实例变量将会在对象实例化时随对象一起分配在Java对中。

假设一个类变量为:public static int count = 10;这时候会分配0,而不是10,分配10是在程序编译后。

第四部分 解析


解析阶段是虚拟机将常量池的符号引用替换为直接引用的阶段

1、类或者接口的的解析

2、字段解析

3、类方法解析

4、接口方法解析

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

第五部分 初始化


在准备阶段,变量已经被分配赋值过初始值,在初始化阶段根据代码的逻辑初始化真实的变量和其他资源。

关于类加载器


什么是类加载器?

在“加载”阶段中,通过一个类的全限定名来获取其定义的二进制字节流。这一动作是放到了Java虚拟机外部去实现的,是为了方便让应用自己去决定如何获取所需要的类,实现这个动作的功能是常说的“类加载器(ClassLoader)”

类加载器主要有三种:

1.启动类加载器(Bootstrap ClassLoader)

负责加载<JAVA_HOME>\lib

2.扩展类加载器(Exension ClassLoader)

负责加载<JAVA_HOME>\lib\ext

3.应用程序类加载器(Applicaion ClassLoader)

负责加载ClassPath上指定的类库

类加载器工作原理

介绍类加载器原理之前,必须得了解双亲委派模型(Parents Delegation Model)

双亲委派模式的工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

如图所示,这种层次结构关系被称为双亲委派模型
以下为其实现代码,集中在java.lang.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)) {
// First, check if the class has already been loaded
//首先检查类是否被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果有父加载器,则先委托父加载,否则由启动类加载器加载,如果启动类加载器没有找到,则返回null
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//这里的ClassNotFoundException来自父加载器
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//在父类Classloader还没办法加载的时候
//再调用本身的findclass方法来加载类
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

代码的逻辑很清楚:

先加载类是否已经被加载过,若没有则调用父的loadClass()方法,如果父 类加载器为空,则使用启动类加载器作为父加载器,如果父 类加载器加载失败,再调用自己的findClass()方法进行加载