你们都知道,当咱们写好一个Java程序以后,都是须要通过编译成若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不一样的class文件当中,因此常常要从这个class文件中要调用另一个class文件中的方法,若是另一个文件不存在的,则会引起系统异常。而程序在启动的时候,并不会一次性加载程序所要用的全部class文件,而是根据程序的须要,经过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存以后,才能被其它class所引用。因此ClassLoader就是用来动态加载class文件到内存当中用的。html
类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要缘由之一。它使得 Java 类能够被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了知足 Java Applet 的须要而开发出来的。Java Applet 须要从远程下载 Java 类文件到浏览器中并执行。如今类加载器在 Web 容器和 OSGi 中获得了普遍的使用。通常来讲,Java 应用的开发人员不须要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够知足大多数状况的需求了。不过若是遇到了须要与类加载器进行交互的状况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundException
和 NoClassDefFoundError
等异常。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这个重要概念。下面首先介绍一些相关的基本概念。java
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。通常来讲,Java 虚拟机使用 Java 类的方式以下:Java 源程序(.java 文件)在通过 Java 编译器编译以后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class
类的一个实例。每一个这样的实例用来表示一个 Java 类。经过此实例的 newInstance()
方法就能够建立出该类的一个对象。实际的状况可能更加复杂,好比 Java 字节代码多是经过工具动态生成的,也多是经过网络下载的。算法
Java 应用环境中不一样的class 分别由不一样的ClassLoader 进行加载,以下:api
一个jvm中默认的classloader有Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader,分别各司其职,除此以外,用户还能够自定义ClassLoader,具体以下:浏览器
称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可经过以下程序得到该类加载器从哪些地方加载了相关的jar或class文件,因为JVM会自动加载这些jar包,因此这些jar包不用在classpath中指定。缓存
package com.jason.classload; import java.net.URL; /** * Created by jason on 2017/11/30. */ public class TestBootStrapClassLoader { public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i].toExternalForm()); } System.out.println(System.getProperty("sun.boot.class.path")); System.out.println(System.getProperty("java.class.path")); System.out.println(System.getProperty("java.ext.dirs")); } }
打印结果以下:tomcat
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/classes安全
从结果中,能够看到,该类加载器加载了jdk中的java基础类,主要是 %JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar等核心类库的jar,另外还有classess。所以该结果也能够经过查找sun.boot.class.path这个系统属性所得知,代码在前面已经贴出来,获得的结果以下:网络
/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/classesapp
负责加载java扩展类,主要是 %JRE_HOME/lib/ext 目录下的或者或者由java.ext.dirs系统属性指定的jar和class
称为应用(也称为系统)类加载器,负责加载应用程序classpath目录下的全部jar和class文件。它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或 者 CLASSPATH操做系统属性所指定的JAR类包和类路径。总能经过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。若是没有特别指定,则用户自定义的任何类加载器都将该类加载器做为它的父加载器。所以该结果也能够经过查找java.class.path这个系统属性所得知,代码在前面已经贴出来,输出结果则为用户在系统属性里面设置的CLASSPATH。
除了Java默认提供的三个ClassLoader以外,用户还能够根据须要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,并重写父类的findClass方法。
其中Bootstrap ClassLoader是JVM级别的,不继承自ClassLoader,由于它不是一个普通的Java类,底层由C++撰写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,而后初始化sun.misc.Launcher ,sun.misc.Launcher构造并初始化Extension ClassLoader、App ClassLoader类加载器。Extension ClassLoader、App ClassLoader都是java类,都继承自URLClassLoader超类。 另外,类加载采用了cache机制,就是每次加载的时候先从缓存中查找,若是能找到,则直接返回,这就是为何每次修改了class时还须要重启JVM才能生效的缘由。
一个classloader加载一个class后,这个class所引用或者依赖的类也由这个classloader载入,除非显示的用另外一个classloader载入。
先由父加载器加载,除非父加载器找不到时才从本身的类路径中去寻找。
Classloader采用缓存机制,即先查Cache;若Cache中保存了这个Class就直接返回;若无,才从文件读取和转化为Class并放入Cache
ClassLoader使用的是双亲委托模型来搜索类的,每一个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)自己没有父类加载器,但能够用做其它ClassLoader实例的的父类加载器。当一个ClassLoader实例须要加载某个类时,它会试图亲自搜索某个类以前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,若是没加载到,则把任务转交给Extension ClassLoader试图加载,若是也没加载到,则转交给App ClassLoader 进行加载,若是它也没有加载获得的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。若是它们都没有加载到这个类时,则抛出ClassNotFoundException异常。不然将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时就被引导类加载器(Bootstrap ClassLoader)加载,因此用户自定义的ClassLoader永远也没法加载一个本身写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
3.4.一、类关系
由图看到Bootstrap ClassLoader并不在继承链上,由于它是java虚拟机内置的类加载器,对外不可见。能够看到顶层ClassLoader
有一个parent属性,用来表示着类加载器之间的层次关系(双亲委派模型);注意,ExtClassLoader
类在初始化时显式指定了parent为null,因此它的父类加载器默认为Bootstrap ClassLoader
。在tomcat中都是经过扩展URLClassLoader
来实现本身的类加载器。
这3种类加载器之间存在着父子关系(区别于java里的继承),子加载器保存着父加载器的引用。当一个类加载器须要加载一个目标类时,会先委托父加载器去加载,而后父加载器会在本身的加载路径中搜索目标类,父加载器在本身的加载范围中找不到时,才会交还给子加载器加载目标类。
采用双亲委托模式能够避免类加载混乱,并且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中同样的类,最终仍是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,因此二者都不会再从新加载。固然,若是使用者经过自定义的类加载器能够强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException
异常。
sun.misc.Launcher
构造函数中被初始化,它的父类加载器被设置了为null,那为何还说启动类加载器是它的父加载器?看一下ClassLoader.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) { // 父类加载器不为null,则调用父类加载器尝试加载 c = parent.loadClass(name, false); } else { // 父类加载器为null,则调用本地方法,交由启动类加载器加载,因此说ExtClassLoader的父类加载器为Bootstrap ClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { //仍然加载不到,只能由本加载器经过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; } }
从代码中看到,若是parent==null,将会由启动类加载器尝试加载,因此扩展类加载器的父类加载器是启动类加载器。
sun.misc.Launcher
构造函数初始化应用程序类加载器时,指定了ExtClassLoader为AppClassLoader的父类加载器:Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader"); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader"); }
protected ClassLoader() { //调用getSystemClassLoader方法获取系统类加载器做为父类加载器 this(checkCreateClassLoader(), getSystemClassLoader()); } public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); //初始化系统类加载器 ..... return scl; } private static synchronized void initSystemClassLoader() { ...... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); ...... scl = l.getClassLoader(); //这里拿到的就是在Launcher构造函数中构造的AppClassLoader实例 ...... } }
JVM在断定两个class是否相同时,不只要判断两个类名是否相同,并且要判断是否由同一个类加载器实例加载的。只有二者同时知足的状况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,若是被两个不一样的ClassLoader实例所加载,JVM也会认为它们是两个不一样class。好比网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译以后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来讲,它们是两个不一样的实例对象,但它们确实是同一份字节码文件,若是试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不一样的类型。如今经过实例来验证上述所描述的是否正确:
在JVM中,如何肯定一个类型实例?答:全类名吗?不是,是类加载器加上全类名
参考文献:
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
参考:http://blog.csdn.net/lovingprince/article/details/4238695
http://www.blogjava.net/crazycy/archive/2007/02/01/97350.html ClassLoader基础
http://www.blogjava.net/crazycy/archive/2006/11/24/83379.html ClassLoader 之 Servlet妙用
http://longdick.iteye.com/blog/442213
http://ifeve.com/classloader/ 深刻浅出ClassLoader
http://www.cnblogs.com/kindevil-zx/p/5603643.html Dubbo源码之SPI