1:what is it
jvm把描述类的数据从class字节码文件加载到内存,并对数据进行校验、解析、初始化,最终成为jvm直接使用的数据类型
1、ClassNotFoundExcetpion
我们在开发中,经常可以遇见java.lang.ClassNotFoundExcetpion这个异常,今天我就来总结一下这个问题。对于这个异常,它实质涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然它和我们直接打交道不多,但是对其背后的机理有一定理解有助于我们排查程序中出现的类加载失败等技术问题。
2、类的加载过程
一个java文件从被加载到被卸载这个生命过程,总共要经历5个阶段,JVM将类加载过程分为:
加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
(1)加载
首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。
(2)链接:
验证:确保被加载类的正确性;
准备:为类的静态变量分配内存,并将其初始化为默认值;
解析:把类中的符号引用转换为直接引用;
(3)为类的静态变量赋予正确的初始值
3、类的初始化
(1)类什么时候才被初始化
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
(2)类的初始化顺序
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
2:when it happens
初始化时机 Jvm规范规定了 有且仅有5种
1new getstatic putstatic invokestatic 4条指令时 (new一个对象 使用静态属性和方法)
2使用java.lang.reflect包的方法对某个类进行反射调用 forname
3 初始化一个类时发现其父类尚未初始化 先初始化父类
4Jvm启动时 加载main方法所在的类
5Jdk1.7的动态语言 MathodHander解析结果为 REF_getStatic REF_putStatic REF_inpokStatic 加载对应类
3:ClassLoader
类加载器 实现类加载的动作的类
3.1jvm中的类加载器:
1.引导类(Bootstrap classloader):组成Java平台的类,包括jre/lib/rt.jar -Xbootclasspath指定 按文件名识别
2.扩展类(Extensions classloader):使用Java扩展机制的类,jre/lib/ext java.ext.dirs指定的其他jar
3.用户类(application classloader 系统、应用类加载器):
由开发者定义的类和没有利用扩展机制的第三方类,这些类的位置由用户指定,
一般通过使用-classpath命令行选项或者使用CLASSPATH环境变量来指定其位置。
3.2层次结构:
Bootstrap > Extension > Application > user classloder
但他们之间不是以继承的关系来实现 而是组合的形式来复用父加载器的方法
每个累加器都有parent属性指向它的上级类加器(见下面代码)
3.3双亲委派模型(见下图)
若一个类加载器收到类加载的请求 他首先自己不会尝试加载这个类 而是把这个请求委托给父类加器去完成
每一个类加器都如此 这样所有的类加载请求都会传入到bootstrap中 只有当父加载器加载不了时
此时父加载器抛出异常,子加载器捕获后 再在自己的领域内尝试加载
比如:
* 代码中出现了这么一行:new A();
> 系统发现了自己加载的类,其中包含了new A(),这说明需要系统去加载A类
> 系统会给自己的领导打电话:让扩展去自己的地盘去加载A类
> 扩展会给自己的领导打电话:让引导去自己的地盘去加载A类
> 引导自己真的去rt.jar中寻找A类
* 如果找到了,那么加载之,然后返回A对应的Class对象给扩展,扩展也会它这个Class返回给系统,结束了!
* 如果没找到:
> 引导给扩展返回了一个null,扩展会自己去自己的地盘,去寻找A类
* 如果找到了,那么加载之,然后返回A对应的Class对象给系统,结束了!
* 如果没找到
> 扩展返回一个null给系统了,系统去自己的地盘(应用程序下)加载A类
* 如果找到了,那么加载之,然后返回这个Class,结束了!
* 如果没找到,抛出异常ClassNotFoundException
好处:java类随着他的类加载器一起具备了一种带有优先级的层次关系 如java.lang,Object类放在rt.jar内
无论哪一个类加载器要加载它,最终都要委托到启动类加载器加载他 因此 在各种类加载器环境中使用Object都是同一个类
如果没有这种委派机制 用 户自定义一个java.lang.Object放在classpath下
加载时系统将会出现多个不同的object类 Java类型体系的最基础的行为就会被破坏掉 程序也将一片混乱
同一个类: 只有2个类由同一个类加载器加载的前提下 他们才可能相等
即 相等条件: 相同的class文件 + 同一个类加载器加载
3.5core code
// The parent class loader for delegation
private ClassLoader parent;
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name);//只有bootstrap会执行这一句 } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
先判断是否已经加载 内存中已有 不再加载
不存在 判断parent加载器是否为null no 则调用父加载器的loadClass方法 否则
该类加载器为bootstrap 他执行自己的加载方法findBootstrapClassOrNull
当父类在自己的领域内找不到时 findClass会抛出异常
子类捕获异常后 就会尝试在自己领域内加载
3.6图解
3.7自定义类加载器
我们也可以通过继承ClassLoader类来完成自定义类加载器,自类加载器的目的一般是为了加载网络上的类,因为这会让class在网络中传输,为了安全,那么class一定是需要加密的,所以需要自定义的类加载器来加载(自定义的类加载器需要做解密工作)。
ClassLoader加载类都是通过loadClass()方法来完成的,loadClass()方法的工作流程如下:
l 调用findLoadedClass ()方法查看该类是否已经被加载过了,如果该没有加载过,那么这个方法返回null;
l 判断findLoadedClass()方法返回的是否为null,如果不是null那么直接返回,这可以避免同一个类被加载两次;
l 如果findLoadedClass()返回的是null,那么就启动代理模式(委托机制),即调用上级的loadClass()方法,获取上级的方法是getParent(),当然上级可能还有上级,这个动作就一直向上走;
l 如果getParent().loadClass()返回的不是null,这说明上级加载成功了,那么就加载结果;
l 如果上级返回的是null,这说明需要自己出手了,这时loadClass()方法会调用本类的findClass()方法来加载类;
l 这说明我们只需要重写ClassLoader的findClass()方法,这就可以了!如果重写了loadClass()方法覆盖了代理模式!
OK,通过上面的分析,我们知道要自定义一个类加载器,只需要继承ClassLoader类,然后重写它的findClass()方法即可。那么在findClass()方法中我们要完成哪些工作呢?
l 找到class文件,把它加载到一个byte[]中;
l 调用defineClass()方法,把byte[]传递给这个方法即可。
public class FileSystemClassLoader extends ClassLoader {
private String classpath ; public FileSystemClassLoader() {} public FileSystemClassLoader (String classpath) {
this.classpath = classpath;
} @Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] datas = getClassData(name);
if(datas == null) {
throw new ClassNotFoundException("类没有找到:" + name);
}
return this.defineClass (name, datas, 0, datas.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类找不到:" + name);
}
} private byte[] getClassData(String name) throws IOException {
name = name.replace(".", "\\") + ".class";
File classFile = new File(classpath, name);
return FileUtils .readFileToByteArray(classFile);
}
} ClassLoader loader = new FileSystemClassLoader("F:\\classpath");
Class clazz = loader.loadClass("cn.itcast.utils.CommonUtils");
Method method = clazz.getMethod("md5", String.class);
String result = (String) method.invoke(null, "qdmmy6");
System.out.println(result);
4tomcat类加器 Tomcat 5
Bootstrap>Extension>Application>Common >shared>webappX>jsperLoader
> Catalina
/common:tomcat和所有webapp共同使用
/server:tomcat使用 webapp不可见
/shared:所以webapp共同使用 tomcat不能用
•Common:该类加载器包含一些对Tomcat内部类和web应用可见的额外类。
其中包括(1)jasper-compiler.jar:JSP 2.0编译器(2)jsp-api.jar:JSP 2.0 API(3)servlet-api.jar:servlet 2.4 API等等。对应文件夹 /common
•Catalina:该加载器初始化用来包含实现Tomcat 5本身所需要所有类和资源;对应文件夹 /server
•Shared:在所有的web应用程序间共享的类和资源;对应文件夹 /shared
•WebappX:为每个部署在单个Tomcat 5实例上的Web应用创建的类加载器。
加载/WEB-INF/classes和WEB-INF/lib下的类和资源。
值得注意的是,Web应用程序类加载器行为与默认的Java 2委派模型不同。当一个加载类的请求被WebappX类加载器处理时,类加载器将首先查看本地库,而非在查看前就委派,
但是也有例外,作为JRE基本类一部分的类不能被覆盖,但是对与一些类,可以使用J2SE 1.4的Endorsed Standards Override机制。最后,任何包含servlet API的JAR包都将被该类加载器忽略。
5Tomcat 6.0:
Bootstrap>Extension>Application>Common > webappX
在tomcat中类的加载稍有不同,如下图:
当tomcat启动时,会创建几种类加载器:
1 Bootstrap 引导类加载器
加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
2 System 系统类加载器
加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。
位于CATALINA_HOME/bin下。
3 Common 通用类加载器
加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
4 webapp 应用类加载器
每个应用在部署后,都会创建一个唯一的类加载器。
该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
当应用需要到某个类时,则会按照下面的顺序进行类加载:
1 使用bootstrap引导类加载器加载
2 使用system系统类加载器加载
3 使用应用类加载器在WEB-INF/classes中加载
4 使用应用类加载器在WEB-INF/lib中加载
5 使用common类加载器在CATALINA_HOME/lib中加载
问题扩展
通过对上面tomcat类加载机制的理解,就不难明白 为什么java文件放在Eclipse中的src文件夹下会优先jar包中的class?
这是因为Eclipse中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,
被编译成class文件放在 WEB-INF/class 中。
而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。
因此肯定是 java文件或者JSP文件编译出的class优先加载。
通过这样,我们就可以简单的把java文件放置在src文件夹中,通过对该java文件的修改以及调试,
便于学习拥有源码java文件、却没有打包成xxx-source的jar包。
另外呢,开发者也会因为粗心而犯下面的错误。
在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此时就会导致某些情况下报加载不到类的错误。
还有如果多个应用使用同一jar包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。