【java虚拟机系列】JVM类加载器与ClassNotFoundException和NoClassDefFoundError
在我们日常的项目开发中,会经常碰到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 开始学 JVM 系列 | JVM 类加载器(一)
从 1 开始学 JVM 系列 类加载器,对于很多人来说并不陌生.我自己第一次听到这个概念时觉得有点"高大上",觉得只有深入 JDK 源码才会触碰到 ClassLoader,平时都是 ...
- 【Java虚拟机3】类加载器
前言 Java虚拟机设计团队有意把类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类. ...
- 虚拟机系列 | JVM类加载机制
本文源码:GitHub·点这里 || GitEE·点这里 一.类加载简介 类的加载机制是指把编译后的.class类文件的二进制数据读取到内存中,并为之创建一个java.lang.Class对象,用来封 ...
- Java虚拟机10:类加载器
类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...
- Java虚拟机14:类加载器
类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...
- JVM 类加载器深入解析以及重要特性剖析
1.类加载流程图 从磁盘加载到销毁的完整过程. 2.类加载流程图2 1.加载: 就是把二进制形式的java类型读入java虚拟机中 2.连接: 验证.准备.解析. 连接就是将已经读入到内存的类的二进制 ...
- Java虚拟机笔记 – JVM 自定义的类加载器的实现和使用2
1.用户自定义的类加载器: 要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定类的名 ...
- 深入了解java虚拟机(JVM) 第十二章 类加载器
一.什么是类加载器 类加载器是一个用来加载类文件的类,Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序.需要注意的是,只有被同一个类加载器加载的类才可能会相等 ...
- 【java虚拟机系列】java虚拟机系列之JVM总述
我们知道java之所以能够快速崛起一个重要的原因就是其跨平台性,而跨平台就是通过java虚拟机来完成的,java虚拟机属于java底层的知识范畴,即使你不了解也不会影响绝大部分人从事的java应用层的 ...
随机推荐
- Trie模版
struct Trie{ Trie* nxt[]; int v; Trie(){ ;i<;i++){ nxt[i]=NULL; } v=-; } void insert(char s[],int ...
- ●BZOJ 2209 [Jsoi2011]括号序列
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2209 题解: Splay 很好的题,但是把智障的我给恶心到了... 首先不难发现,最后没 ...
- hdu 5667 BestCoder Round #80 矩阵快速幂
Sequence Accepts: 59 Submissions: 650 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536 ...
- 网络硬盘NFS
NFS是网络文件系统,用于计算机间共享文件系统,由sun公司1985年推出的协议,现在已经被广泛使用.一般来说,所有的linux发型版都支持NFS.nfs是一个服务器,客户端的架构,建立一个nfs的服 ...
- Python作业之多级菜单
作业之多级菜单 菜单实现要求: 1. 列出菜单选择供选择 2. 选择对应内容进入下一级菜单 3. 任何时候都可以选择退出程序或返回上一级菜单 具体代码如下: goods = {'华为':{'A系':{ ...
- 手写JAVA虚拟机(二)——实现java命令行
查看手写JAVA虚拟机系列可以进我的博客园主页查看. 我们知道,我们编译.java并运行.class文件时,需要一些java命令,如最简单的helloworld程序. 这里的程序最好不要加包名,因为加 ...
- Jenkins执行批处理文件、powershell失败
今天搭建Jenkins持续集成环境,编译环境是.net core.整理了一些发布的命令配置在Jenkins,问题来了,使用powershell插件运行dotnet restore.dotnet bui ...
- kafka topic 相关操作
1.列出集群中的topic bin/kafka-topics.sh --zookeeper spark1:2181,spark2:2181,spark3:2181 --list 2.创建topic r ...
- Zookeeper和Chubby【分布式协调系统】
前言(对于协调系统来说其客户端往往是分布式集群) 大规模分布式系统需要解决各种类型的协调需求: 当集群中有新的进程或服务器加入时,如何探测到它的加入?如何能够自动获取配置参数? 当配置信息被某个进程或 ...
- Java面试06|项目相关介绍
1.明确你的项目到底是做什么的,有哪些功能 广告投放机:项目主要是为移动端有针对性的进行广告展示. 媒体管理平台SSP:为媒体端实现多种变现途径 (1)广告投放机中关于广告检索与排序的功能 1.广告检 ...