前言
之前总结的是Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都要加载到虚拟机中之后才能运行和使用。那么虚拟机是如何加载这些Class文件的,Class文件中的信息进入到虚拟机后会发生什么变化,这些将会在本篇进行总结。
1、概述
什么是虚拟机的类加载机制?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
Java语言的动态加载和动态连接?
java语言中类型的加载连接以及初始化过程都是在程序运行期间完成的,这种策略虽然会使类加载时稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性。java里天生就可以动态扩展语言特性就是依赖运行期间动态加载和动态连接这个特点实现的。比如,如果编写一个面向接口的程序,可以等到运行时再指定其具体实现类。
2、类加载的时机
2.1 类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括以下七个阶段,其中验证、准备、解析阶段统称为连接。

2.2 必须对类进行“初始化”的五种情况
- 使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,已经调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类没有被初始化就会先初始化它的父类。
- 当虚拟机启动的时候,用户需要指定一个要执行的主类(就是包含main()方法的那个类),虚拟机会先初始化这个类。
- 使用Jdk1.7动态语言支持的时候的一些情况。
3、类加载过程
下面详细说说这几个阶段的加载过程。
3.1 加载
在加载阶段,虚拟机需要完成以下三件事:
通过类的全限定名,产生一个代表该类的二进制数据流(根本没有指明从哪里获取、怎样获取,可以说一个非常开放的平台了)
解析这个二进制数据流为方法区内的运行时数据结构
创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口。
这里解释一下什么是Class类:在Java中有两种对象:Class对象和实例对象,实例对象是类的实例,通常是通过
new关键字构建的。Class对象是JVM生成用来保存对象的类的信息的。Java程序执行之前需要经过编译、加载、链接和初始化这几个阶段,编译阶段会将源码文件编译为.class字节码文件,编译器同时会在.class文件中生成Class对象,加载阶段通过JVM内部的类加载机制,将Class对象加载到内存中。在创建对象实例之前,JVM会先检查Class对象是否在内存中存在,如果不存在,则加载Class对象,然后再创建对象实例,如果存在,则直接根据Class对象创建对象实例。JVM中只有一个Class对象,但可以根据Class对象生成多个对象实例。
3.2 连接
验证
验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。
验证阶段会完成以下4个阶段的检验动作:
1)文件格式验证
2)元数据验证(是否符合Java语言规范)
3)字节码验证(确定程序语义合法,符合逻辑。主要是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件)
4)符号引用验证(确保下一步的解析能正常执行)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,类变量初始值在通常情况下是数据类型的零值,比如
1 >public static int value = 123;这个value就是类变量,并且在该阶段过后的初始值为0而不是123,把value赋值为123是在初始化阶段才会执行。但是有一种特殊情况就是,如果字段的字段属性表中存在ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,例如
1 >public static final int value = 123;解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:这个词在上一篇类文件结构中出现过多次,主要是CONSTANT_Class_info等等这种类型的常量。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在了。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。前面四种引用的解析过程,对于后面三种,与JDK1.7新增的动态语言支持息息相关,由于java语言是一门静态类型语言,因此没有介绍invokedynamic指令的语义之前,没有办法将他们和现在的java语言对应上。
3.3 初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
4、类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。
4.1 双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分。另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器。
- 启动类加载器:这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
- 扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载,如果有必要还可以加入我们自己定义的类加载器。这些加载器之间的关系一般如图所示。也称为双亲委派模型。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成的这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
那么这么使用的好处是什么?
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。类
java.lang.Object(存放在rt.jar中)在加载过程中,无论哪一个类加载器要加载这个类,最终需委派给模型顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。若没有使用双亲委派模型(即由各个类加载器自行去加载)、用户编写了一个
java.lang.Object的类(放在ClassPath中),那系统中将出现多个不同的Object类,Java体系中最基础的行为就无法保证。
更新 2019年12月19日
今天在看spi的时候,回过头再看双亲委派机制。为什么说双亲委派机制有缺陷呢?或者说在什么情况下要打破双亲委派机制?我们知道双亲委派机制中加载器的相互调用是有顺序的,只能由下往上调用,也就是从应用程序类加载器往上调用,而不能从上往下进行调用。而如果基础类要调用用户的代码该怎么办呢?也就是由上往下进行调用,这时候双亲委派机制就被破坏了。这种情况其实很多,比如这里的spi,还有jdbc等等。其中jdbc我们知道是一个数据库的连接标准,是一组类和接口,而具体接口的实现就需要每个数据库厂商完成,所以这里就有了自上而下的调用,破坏双亲委派机制。