Java类加载机制与类加载器
一、类加载机制
1、什么是类的加载?
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在java堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向程序员提供了访问方法区内的数据结构的接口。
2、类的加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。顺序如下图所示:

其中,类的加载过程包括了加载、验证、准备、解析、初始化五个阶段,验证、准备、解析三个阶段统称为连接。
- 加载(查找并加载类的二进制数据)
加载是类加载机制中的第一个阶段。在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的静态存储结构转为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,而且在Java堆中创建一个java.lang.Class对象,这样就可以通过该对象访问方法区中的数据。
- 验证
验证是连接阶段的第一步,它的主要作用是确保被加载的类的正确性。即为了确保加载的.class文件不能对虚拟机有危害,所以先检测验证一下。主要完成四个阶段的验证:
(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。包含了魔数、主版本号、常量池等等校验
(2)元数据验证:主要是对字节码描述的信息进行语义分析,以确保其描述的信息符合Java语言的语法规范。如:验证是不是有父类,类中的字段方法是不是和父类冲突等等
(3)字节码验证:是整个验证过程中最复杂的阶段,主要是通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,字节码验证主要是对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。
(4)符号引用验证:是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验,目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个重要但是非必需的阶段。如果代码能够确保没有问题,那么就没有必要去验证,毕竟验证是需要花费一定的时间。
- 准备
准备阶段主要是为类的静态变量分配内存,并设置默认初始值。这些内存都在方法区分配。
需要注意两个关键点,即内存分配的对象和初始化的类型。
(1)内存分配的对象:Java中的变量有[类变量]和[类成员变量]两种类型,[类变量]指的是被static修饰的变量,而其他所有类型的变量都属于[类成员变量]。在准备阶段,虚拟机只会为[类变量]分配内存,不会为[类成员变量]分配内存。[类成员变量]的内存分配需要等到初始化阶段才开始。
例如下面的代码,在准备阶段只会为age属性分配内存,而不会为name属性分配内存。
public static int age = 28; public String name = "chdf";
(2)初始化的类型:在准备阶段,虚拟机会为类变量分配内存,并为其初始化。这里的初始化指的是为变量赋予Java语言中该数据类型的默认值,而不是程序代码里初始化的值。
例如下面的代码在准备阶段之后,age的值将是0,而不是28。
public static int age = 28;
注意:如果一个变量是常量的话,那么在准备阶段,属性便会被赋予用户希望的值。
例如下面的代码在准备阶段之后,age的值将是28,而不是0。
public static final int age = 28;
- 解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
(1)符号引用:符号引用就是对引用对象的一个由符号组成的描述符,通过符号引用可以定位到目标。在编译阶段编译器并不知道引用对象的地址所以只能用符号引用来定位目标。
(2)直接引用:直接引用就是目标的地址或者能找到对象的句柄的地址。
- 初始化
初始化是加载机制的最后一步。在初始化阶段,主要是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
(1)声明类变量时指定初始值
(2)使用静态代码块为类变量指定初始值
JVM初始化步骤:
(1)假如这个类还没有被加载和连接,则程序先加载并连接该类
(2)假设该类的父类还没有被初始化,则先初始化其直接父类
(3)假设类中有初始化语句,则系统依次执行这些初始化语句
3、类的加载时机
Java中对类加载的时机没有强制要求,但是对类初始化的时机有具体的要求。一般来说,JVM遇到以下5种情况时会触发初始化:
(1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。这4条指令常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
(3)初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
(5)如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化
简而言之,即以下几种情况:
(1)创建类的实例,也就是通过new的方式
(2)访问某个类或接口的静态变量,或者对该静态变量赋值
(3)调用类的静态方法
(4)反射,如Class.forName("com.chdf.Test")
(5)初始化某个类的子类,则其父类也会被初始化
(6)Java虚拟机启动时被标明为启动类的类,即文件名和类名相同的那个类
4、类加载机制
JVM的类加载机制主要有如下3种:
(1)全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
(2)双亲委派:所谓双亲委派,就是当一个类加载器接收到类加载请求时,它会把这个请求委派给父加载器去完成,依次递归,因此所有的加载请求最终都被委派给顶层的启动类加载器中,如果父加载器可以完成类加载任务,就成功返回,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
(3)缓存机制:缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个类时,类加载器先从缓存区中搜寻该类,若搜寻不到将读取该类的二进制数据,并转换成Class对象存入缓存区中。这就是为什么修改了Class后需要重启JVM才能生效的原因。
5、双亲委派机制
双亲委派机制要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器(注意:双亲委派机制中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码)。类加载器之间的关系如下图:

工作原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了,儿子才想办法自己去完成。
优势:采用双亲委派机制的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次。其次是考虑到安全因素,如果不使用这种机制,就可以随时使用自定义的类库来动态替换Java核心api中的定义类型,这样就会存在安全隐患。通过双亲委派机制,可以避免这种情况,防止核心API库被随意篡改。
二、类加载器
虚拟机的设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为类加载器。
只有被同一个类加载器加载的类才可能会相等,相等的字节码被不同的类加载器加载的类不相等。
1、类加载器分类
(1)启动类加载器Bootstrap ClassLoader
用来加载Java的核心类,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
负责加载$JAVA_HOME/jre/lib目录中的类库,由C++实现,不是ClassLoader子类。
(2)扩展类加载器Extension ClassLoader
负责加载JRE扩展目录($JAVA_HOME/jre/lib/ext)下的jar包类以及java.ext.dirs系统变量指定的路径中类库。由Java语言实现,父类加载器为null
(3)应用程序类加载器Application ClassLoader
负责加载CLASSPATH环境变量下指定的jar包及目录中class。一般情况下该类加载器是Java程序默认的类加载器。由Java语言实现,父类加载器为Extension ClassLoader
2、类加载器加载Class步骤
类加载器加载Class大致经过如下8个步骤:
(1)检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步
(2)如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步
(3)请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步
(4)请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步
(5)当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步
(6)从文件中载入Class,成功后跳至第8步
(7)抛出ClassNotFountException异常
(8)返回对应的java.lang.Class对象
浙公网安备 33010602011771号