前言:我们在开发中,经常可以遇见java.lang.ClassNotFoundExcetpion这个异常,对于这个异常,它实质涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然它和我们直接打交道不多,但是对其背后的机理有一定理解有助于我们排查程序中出现的类加载失败等bug。
一、类加载过程定义
Java类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
二、类加载过程详介
Java类加载过程即类装载器把一个类装入Java虚拟机中,总体来说包含以下过程:
加载 -> 链接(验证+准备+解析)->初始化(使用前的准备)
2.1、加载
⑴通过一个类的全限定名来获取定义此类的二进制字节流。
⑵将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
⑶在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
注意:第一步获取二进制字节流有很多形式,因为它并没有限定二进制流从哪里来,那么我们可以用系统的类加载器,也可以用自己的方式写加载器来控制字节流的获取:
①从class文件来->一般的文件加载
②从zip包中来->加载jar中的类
③从网络中来->Applet
获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在Java堆中实例化一个java.lang.Class对象与方法区中的数据关联起来。
2.2、链接:验证+准备+解析
⑴验证:检查加载类的正确性
⑵准备:为静态变量分配内存地址,并将其初始化为默认值
⑶解析:将符号引用转为直接引用
第一步:验证
验证又可以细分为几个步骤: 文件格式验证->元数据验证->字节码验证->符号引用验证
①文件格式验证:验证字节流是否符合Class文件格式的规范并 验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。
②元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。
③字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。
④符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)
验证的目的:确保class文件的字节流信息符合jvm的规范。假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm完全崩溃。
验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。
第二步:准备
这阶段会为类变量(静态变量)分配内存并设置初始默认值,这些内存在方法区中进行分配。注意这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。
例如:
public static int value=123; //此时value的值为0,不是123。
private int i = 123; //此时,i还未进行初始化,因为这句代码还不能执行。
第三步:解析
虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用就是class文件中:CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量
符号引用和直接引用的概念:
①符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
②直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
2.3、初始化:激活类的静态变量和静态代码块,初始化Java代码
初始化阶段是类加载的最后一个阶段,前面的几个类加载阶段中,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始化阶段,才开始真正执行类中定义的Java程序代码。
要对类进行初始化 ,代码上可以理解为:为要初始化的类中的所有静态成员都赋予初始值、对类中所有静态块都执行一次,并且是按代码编写顺序执行。
如下代码:输出的是1。如果①和②顺序调换,则输出的是123。
public class Main {
public static void main(String[] args){
System.out.println(Super.i);
}
}
class Super{
//①
static{
i = 123;
}
//②
protected static int i = 1;
}
(1)类什么时候才被初始化
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
(2)类的初始化顺序
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
三、类加载器分类
3.1、ClassLoader:通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。等到程序运行时,JVM先初始化,在JVM初始化的过程中,JVM生成几个ClassLoader,JVM调用指定的ClassLoader去加载.class文件等各类路径、文件的类。
程序运行时类的加载实际过程:
①JDK执行指令去寻找jre目录,寻找jvm.dll,并初始化JVM;产生一个Bootstrap Loader(启动类加载器);
②Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
③Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
④最后由AppClass Loader加载Java类。
3.2、JVM的类加载是通过ClassLoader及其父类ClassLoader来完成的,类的层次关系和加载顺序可以由下图来描述:
加载器介绍 :
1)BootstrapClassLoader(启动类加载器):负责加载java核心类库,无法被java程序直接引用
2)ExtensionClassLoader(标准扩展类加载器):负责加载 Java 平台的扩展功能库
3)AppClassLoader(系统类加载器):根据Java 应用的类路径(classpath)来加载指定的jar包和Java 类。一般来说,Java 应用的类都是由它来完成加载的。
4)CustomClassLoader(自定义加载器):通过继承java.lang.ClassLoader类的方式实现 。属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。
3.3、类加载器的顺序
1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。
3.4、类加载器的相关机制
-
全盘负责
委托机制
:即指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。 -
双亲委派机制:先让parent(父)类加载器 寻找,只有在parent找不到的时候才从自己的类路径中去寻找。
-
类加载还采用了
cache机制
:如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么修改了Class但是必须重新启动JVM才能生效,并且类只加载一次的原因。可以避免类的重复加载。
四、双亲委派机制
4.1、双亲委派机制的概念
双亲委派机制:JVM加载类的时候,会先委托父加载器在其路径下寻找目标类,父加载器找不到再委托上层父加载器加载,如果最上层的父加载器仍然没有找到目标类,那么则会交由该类加载器在自己的类路径中查找并载入目标类。
双亲委派模式的工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这就是传说中的双亲委派模式。
4.2、JVM的类加载器具有层级关系,关系如下:
4.3、从AppClassLoader的loadClass源码理解双亲委派
用源码学习双亲委派机制是最适合的,下面是AppClassLoader的loadClass方法:
public synchronized Class loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
try {
// 这里调用了ClassLoader类的loadClass方法
return super.loadClass(var1, var2);
} catch (ClassNotFoundException var5) {
throw var5;
} catch (RuntimeException var6) {
throw var6;
} catch (Error var7) {
throw var7;
}
}
代码中关键的处调用了父类(ClassLoader)的loadClass方法,下面是loadClass方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有上级类加载器且没有加载类
if (parent != null) {
// 调用父加载器的loadClass()方法
c = parent.loadClass(name, false);
} else {
// 没有父加载器,则当前加载器为引导类加载器,该方法最后为C++执行的本地方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍然没有加载到,调用URLclassLoader重写的findClass()方法,在当前类加载器路径中寻找需要加载的类
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
说明:
当父加载器都没找到类的时候就会执行classLoader的findClass()方法,该方法在classLoader中没有实现,需要子类重写实现APPClassLoader继承自UrlClassLoader,而UrlClassLoader重写了该方法,APPClassLoader调用父类的findClass方法。其方法的主要作用为:从URL搜索路径中查找并加载具有指定名称的类。所有引用JAR文件的URL都会根据需要加载并打开,直到找到该类为止。
4.4、双亲委派模型的作用
沙箱安全机制:即使我们自己重写了一个String类也不会被类加载器所加载,保证JDK核心类的优先加载,可以防止核心库被篡改影响全局代码。
主要作用是保证JDK核心类的优先加载。
-
自定义类加载器,重写loadClass方法;
-
使用线程上下文类加载器;
五、总结
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆中创建一个这个类的java.lang.Class对象,用来封装类在方法区的数据。类的加载的最终产品是位于堆中的Class对象,其封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
注意:ClassLoader的loadClass(String className);方法只会加载并编译某类,并不会对其执行初始化
补充:
Java绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对Java来说,绑定分为静态绑定和动态绑定。
①静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对Java,简单的可以理解为程序编译期的绑定。Java 当中的方法只有 final,static,private 和构造方法是前期绑定的。
②动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在Java中,几乎所有的方法都是后期绑定的。
参考资料: