在我们日常的项目开发中,会经常碰到ClassNotFoundException和NoClassDefFoundError这两种异常,对于经验足够的工程师而言,可能很轻松的就可以解决,但是却不一定明白为何要去这么做,本博客将从java虚拟机类加载的角度让大家彻底理解ClassNotFoundException和NoClassDefFoundError这两种异常及一些重用的解决方案。

在博客中我们已经讲解过,程序员定义的java类要被java虚拟机运行首先要做的就是类加载过程,而类加载过程的第一步就是“加载”过程(注意加载只是类加载的一个一个阶段而已,不要混淆这两个不同的概念)。在”加载“阶段,虚拟机需要完成以下三件事:

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

2.将字节流代表的静态存储结构转换为方法区的运行时数据结构/

3.在内存中创建一个代表此类的java.lang.Class对象,作为方法区此类的'各种数据的访问入口。

其中完成第一步动作的代码模块就是java类加载器,顾名思义:类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。下面对类加载器进行详细讲解。



一java.lang.ClassLoader类

基本上java中的类加载器都是继承自java.lang.ClassLoader类的,所以首先我们来介绍一下java.lang.ClassLoader类,然后再介绍java中的几种类加载器。

java.lang.ClassLoader类的作用就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。我们在此只关心和类加载相关的功能,首先我们来看一下java.lang.ClassLoader类中和类加载相关的重要方法:

注意defineClass方法存在多个重载版本,其中一个会抛出NoClassDefFoundError异常,而loadClass()抛出的是 java.lang.ClassNotFoundException异常。

二类加载器的树状组织结构



Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是用户自定义的。系统提供的类加载器主要包括以下三个:

引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,不是继承自 java.lang.ClassLoader。

扩展类加载器(extensions class loader):即ExtClassLoader,它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找加载 Java 类,继承自 java.lang.ClassLoader。

系统类加载器(system class loader):即AppClassLoader,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它,该类继承自 java.lang.ClassLoader。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过  getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于用户自定义的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,用户自定义的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。用图表示如下:

在上述图示中,给人的直观感觉是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,真的是这样吗?我们通过代码来测试一下:

public class LoaderTest {

    public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}

程序的运行结果如下:

sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null

我们知道通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器,那么从运行结果来看,系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,不是预期的启动类加载器,这是为何呢?这就需要我们从java.lang.ClassLoader中的loadClass(String name)方法的源代码就找答案了:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
} protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException { // 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就通过启动类加载器加载该类,
//通过调用本地方法native findBootstrapClass0(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

从上述代码可以很清楚的看到如果该加载器的父类为null则会调用启动类加载器类加载类,除非启动类加载器不能完成加载任务i,才会调用自定义的加载器,因此可知将扩展类的父加载器设置为null与将其父加载器设置为启动类加载器效果是一样的。

三类加载器的双亲委派规则(代理模式)

双亲委派规则指的是如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传递到顶层的启动类加载器去加载,如果父加载器不能完成这个加载请求(在它的搜索范围内找不到所需的类),子加载器才会自己尝试去加载。



那么java中为何会采取双亲委派原则呢?(其实这种原则在各种程序设计中很常见,如安卓中的事件分派机制),要知道这个原因,首先我们需要知道在java中虚拟机是如何判断两个类为同一个类的,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。当且仅当两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器
ClassLoaderX和 ClassLoaderY分别读取了这个 Sample.class文件,然后定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。如果试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

了解了这一点之后,就可以理解采用双亲委派规则的原因了。采用双亲委派规则是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过采用双亲委派规则,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了
Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

四类加载器与ClassNotFoundException和NoClassDefFoundError

通过前面的讲解我们已经知道类加载器采用的是双亲委派原则,类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类
com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。 java.lang.ClassNotFoundException是被loadClass()抛出的, java.lang.NoClassDefFoundError是被defineClass()抛出。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

通过前面的讲解我们知道loadClass()是来启动类的加载过程的,其源代码在前面我们已经分析过,即当父加载器不为null同时不能完成加载请求的情况下会抛出ClassNotFoundException异常,而导致父加载器无法完成加载的一个原因很简单就是找不到这个类,通常这种情况是传入的类的字符串名称书写错误,如调用class.forName(String name)或者loadClass(String name)时传入了一个错误的类的名称导致类加载器无法找到这个类。

defineClass是用来完成类的加载工作的,此时已经表明类加载的启动已经完成了,即当前执行的类已经编译,但运行时找不到它的定义时(class definition existed when the currently executing class was compiled, but the definition can no longer be found.)就会抛出NoClassDefFoundError异常,这种情况通常出现在创建一个对象实例的时候(creating a new instance),如在类X中定义了一个类Y,如在类X中定义如下语句ClassY
y=new ClassY;程序运行成功之后(此时X与Y的字节码文件已经存在),如果将类Y的字节码文件删除了重新运行上述代码,则会在运行时候抛出NoClassDefFoundError异常。当然这只是为了说明抛出这种异常的原因,一般不会出现删除该类字节码情况,实际上是其它原因导致类似删除的效果导致的,如JAR重复引入,版本不一致导至。因为jar中都是一些已经编译好的Class文件,如果存在多个版本那么在加载的时候就不知道应该调用哪一个版本(相当于删除字节码的效果),此种情况一般出现在引入第三方SDK的时候。

再如类X引用类Y时,类Y初始化失败时也会导致以上的错误出现。其实上面说的重复引入第三方SDK就属于这种情况的一种,一般在我们自己的java文件中import第三方的java类,如果存在多个版本那么在加载的时候就不知道应该调用哪一个版本(相当与引入的类Y初始化失败),当然除了第三方SDK外,自己定义的java文件可能也会出现这种情况。

以上就是本博客的主要内容,如果读者觉得不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!

【java虚拟机系列】JVM类加载器与ClassNotFoundException和NoClassDefFoundError的更多相关文章

  1. 从 1 开始学 JVM 系列 | JVM 类加载器(一)

    从 1 开始学 JVM 系列 类加载器,对于很多人来说并不陌生.我自己第一次听到这个概念时觉得有点"高大上",觉得只有深入 JDK 源码才会触碰到 ClassLoader,平时都是 ...

  2. 【Java虚拟机3】类加载器

    前言 Java虚拟机设计团队有意把类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类. ...

  3. 虚拟机系列 | JVM类加载机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.类加载简介 类的加载机制是指把编译后的.class类文件的二进制数据读取到内存中,并为之创建一个java.lang.Class对象,用来封 ...

  4. Java虚拟机10:类加载器

    类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...

  5. Java虚拟机14:类加载器

    类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...

  6. JVM 类加载器深入解析以及重要特性剖析

    1.类加载流程图 从磁盘加载到销毁的完整过程. 2.类加载流程图2 1.加载: 就是把二进制形式的java类型读入java虚拟机中 2.连接: 验证.准备.解析. 连接就是将已经读入到内存的类的二进制 ...

  7. Java虚拟机笔记 – JVM 自定义的类加载器的实现和使用2

    1.用户自定义的类加载器: 要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定类的名 ...

  8. 深入了解java虚拟机(JVM) 第十二章 类加载器

    一.什么是类加载器 类加载器是一个用来加载类文件的类,Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序.需要注意的是,只有被同一个类加载器加载的类才可能会相等 ...

  9. 【java虚拟机系列】java虚拟机系列之JVM总述

    我们知道java之所以能够快速崛起一个重要的原因就是其跨平台性,而跨平台就是通过java虚拟机来完成的,java虚拟机属于java底层的知识范畴,即使你不了解也不会影响绝大部分人从事的java应用层的 ...

随机推荐

  1. 2015 多校联赛 ——HDU5353(构造)

    Each soda has some candies in their hand. And they want to make the number of candies the same by do ...

  2. 2015 多校联赛 ——HDU5303(贪心)

    Delicious Apples Time Limit: 5000/3000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Other ...

  3. CCA更新流程分析

    1 CCA CCA(空间信道评估)在CSMA/CA中比较非常重要,事关整机吞吐量,所以对其实现进行简单分析.CCA好像应该有2种:CCA-CS,是属于PLCP层的,捕获到能量且能量值高于-82dB后, ...

  4. SpringCloud学习之soa基础

    一.soa简单介绍 1)面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来.SOA是解决复杂业务模块,提高扩展性,维护性,可伸缩 ...

  5. salt基本使用之一(1)

    1.帮助文档 以cmd函数为例子说下salt中的一些帮助信息 # 查看指定modules用法 salt '*' sys.doc cmd # 查看所有的modules列表 salt '*' sys.li ...

  6. PHP中利用DOM和simplxml读取xml文档

    实例  用DOM获取下列xml文档中所有金庸小说的书名,该xml文档所在位置为 ./books.xml: <?xml version="1.0" encoding=" ...

  7. Linux下的crontab定时、执行任务命令详解 oracle 自动备份

    在LINUX中,周期执行的任务一般由cron这个守护进程来处理[ps -ef|grep cron].cron读取一个或多个配置文件,这些配置文件中包含了命令行及其调用时间.cron的配置文件称为&qu ...

  8. JVM内存模型及分区

    Java虚拟机在程序执行过程会把jvm的内存分为若干个不同的数据区域来管理,这些区域有自己的用途,以及创建和销毁时间. JVM内存模型如下图所示: jvm管理的内存区域包括以下几个区域:  栈区: 栈 ...

  9. 【python进阶】Garbage collection垃圾回收2

    前言 在上一篇文章[python进阶]Garbage collection垃圾回收1,我们讲述了Garbage collection(GC垃圾回收),画说Ruby与Python垃圾回收,Python中 ...

  10. Jmeter(一)_环境部署

    简介: Apache JMeter是100%纯JAVA桌面应用程序,被设计为用于测试CS/BS的软件.它可以用来测试静态和动态资源的性能,可用于模拟大量负载来测试一台服务器,网络或者对象的健壮性或者分 ...