申明:本文首发于 详细深刻分析 ClassLoader 工做机制 ,若有转载,注明原出处便可,谢谢配合。java
你们都知道,当咱们写好一个 Java 程序以后,不是管是 C/S 仍是 B/S 应用,都是由若干个 .class 文件组织而成的一个完整的 Java 应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不一样的 class 文件当中,因此常常要从这个 class 文件中要调用另一个 class 文件中的方法,若是另一个文件不存在的,则会引起系统异常。而程序在启动的时候,并不会一次性加载程序所要用的全部class文件,而是根据程序的须要,经过Java的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存以后,才能被其它 class 所引用。因此 ClassLoader 就是用来动态加载 class 文件到内存当中用的。web
负责将 Class 加载到 JVM 中算法
审查每一个类由谁加载(父优先的等级加载机制)数据库
将 Class 字节码从新解析成 JVM 统一要求的对象格式apache
为了更好的理解类的加载机制,咱们来深刻研究一下 ClassLoader
和他的方法。api
public abstract class ClassLoader
数组
ClassLoader类是一个抽象类,sun公司是这么解释这个类的:安全
/** * A class loader is an object that is responsible for loading classes. The * class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to * locate or generate data that constitutes a definition for the class. A * typical strategy is to transform the name into a file name and then read a * "class file" of that name from a file system. **/
大体意思以下:服务器
class loader 是一个负责加载 classes 的对象,ClassLoader 类是一个抽象类,须要给出类的二进制名称,class loader 尝试定位或者产生一个 class 的数据,一个典型的策略是把二进制名字转换成文件名而后到文件系统中找到该文件。网络
如下是 ClassLoader 经常使用到的几个方法及其重载方法:
ClassLoader
defineClass(byte[], int, int) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实
例。这个方法被声明为final的
findClass(String name) 查找名称为 name的类,返回的结果是java.lang.Class类的实例
loadClass(String name) 加载名称为 name的类,返回的结果是java.lang.Class类的实例
resolveClass(Class<?>) 连接指定的 Java 类
其中 defineClass 方法用来将 byte 字节流解析成 JVM 可以识别的 Class 对象,有了这个方法意味着咱们不只仅能够经过 class 文件实例化对象,还能够经过其余方式实例化对象,若是咱们经过网络接收到一个类的字节码,拿到这个字节码流直接建立类的 Class 对象形式实例化对象。若是直接调用这个方法生成类的 Class 对象,这个类的 Class 对象尚未 resolve ,这个 resolve 将会在这个对象真正实例化时才进行。
接下来咱们看loadClass
方法的实现方式:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } 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. 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; } }
该方法大概意思:
使用指定的二进制名称来加载类,这个方法的默认实现按照如下顺序查找类: 调用findLoadedClass(String) 方法检查这个类是否被加载过 使用父加载器调用 loadClass(String) 方法,若是父加载器为 Null,类加载器装载虚拟机内置的加载器调用 findClass(String) 方法装载类, 若是,按照以上的步骤成功的找到对应的类,而且该方法接收的 resolve 参数的值为 true,那么就调用resolveClass(Class) 方法来处理类。 ClassLoader 的子类最好覆盖 findClass(String) 而不是这个方法。 除非被重写,这个方法默认在整个装载过程当中都是同步的(线程安全的)。
BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可经过以下程序得到该类加载器从哪些地方加载了相关的jar或class文件:
public class BootStrapTest { 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()); } } }
如下内容是上述程序从本机JDK环境所得到的结果:
其实上述结果也是经过查找 sun.boot.class.path 这个系统属性所得知的。
System.out.println(System.getProperty("sun.boot.class.path"));
打印结果:C:\Java\jdk1.8.0_60\jre\lib\resources.jar;C:\Java\jdk1.8.0_60\jre\lib\rt.jar;C:\Java\jdk1.8.0_60\jre\lib\sunrsasign.jar;C:\Java\jdk1.8.0_60\jre\lib\jsse.jar;C:\Java\jdk1.8.0_60\jre\lib\jce.jar;C:\Java\jdk1.8.0_60\jre\lib\charsets.jar;C:\Java\jdk1.8.0_60\jre\lib\jfr.jar;C:\Java\jdk1.8.0_60\jre\classes
Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。默认加载JAVA_HOME/jre/lib/ext/
目下的全部jar。
App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下的全部jar和class文件。通常来讲,Java 应用的类都是由它来完成加载的。能够经过 ClassLoader.getSystemClassLoader()
来获取它。
除了系统提供的类加载器之外,开发人员能够经过继承java.lang.ClassLoader
类的方式实现本身的类加载器,以知足一些特殊的需求。
除了引导类加载器以外,全部的类加载器都有一个父类加载器。 给出的 getParent()方法能够获得。对于
系统提供的类加载器来讲,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来讲,其父类加载器是加载此类加载器 Java 类的类加载器。由于类加载器 Java 类如同其它的 Java 类同样,也是要由类加载器来加载的。通常来讲,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器经过这种方式组织起来,造成树状结构。树的根节点就是引导类加载器。
ClassLoader
使用的是双亲委托模型来搜索类的,每一个ClassLoader
实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader
)自己没有父类加载器,但能够用做其它ClassLoader实例的的父类加载器。当一个ClassLoader
实例须要加载某个类时,它会试图亲自搜索某个类以前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader
试图加载,若是没加载到,则把任务转交给Extension ClassLoader
试图加载,若是也没加载到,则转交给App ClassLoader
进行加载,若是它也没有加载获得的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。若是它们都没有加载到这个类时,则抛出ClassNotFoundException
异常。不然将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader
再加载一次。考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时就被引导类加载器(Bootstrcp ClassLoader
)加载,因此用户自定义的ClassLoader
永远也没法加载一个本身写的String,除非你改变JDK中ClassLoader
搜索类的默认算法。
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
,提示这是两个不一样的类型。如今经过实例来验证上述所描述的是否正确:
1)、在web服务器上建一个org.classloader.simple.NetClassLoaderSimple.java
类
public class NetClassLoaderSimple { private NetClassLoaderSimple instance; public void setNetClassLoaderSimple(Object object){ this.instance = (NetClassLoaderSimple)object; } }
org.classloader.simple.NetClassLoaderSimple
类的setNetClassLoaderSimple
方法接收一个Object类型参数,并将它强制转换成org.classloader.simple.NetClassLoaderSimple
类型。
2)、测试两个class是否相同 NetWorkClassLoader.java
package classloader; public class NewworkClassLoaderTest { public static void main(String[] args) { try { //测试加载网络中的class文件 String rootUrl = "http://localhost:8080/httpweb/classes"; String className = "org.classloader.simple.NetClassLoaderSimple"; NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl); NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl); Class<?> clazz1 = ncl1.loadClass(className); Class<?> clazz2 = ncl2.loadClass(className); Object obj1 = clazz1.newInstance(); Object obj2 = clazz2.newInstance(); clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } } }
首先得到网络上一个class文件的二进制名称,而后经过自定义的类加载器NetworkClassLoader建立两个实例,并根据网络地址分别加载这份class,并获得这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再经过反射调用clazz1中的setNetClassLoaderSimple方法。
3)、查看测试结果
结论:从结果中能够看出,运行时抛出了java.lang.ClassCastException
异常。虽然两个对象obj1和 obj2的类的名字相同,可是这两个类是由不一样的类加载器实例来加载的,因此JVM认为它们就是两个不一样的类。
了解了这一点以后,就能够理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。全部 Java 应用都至少须要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类须要被加载到 Java 虚拟机中。若是这个加载过程由 Java 应用本身的类加载器来完成的话,极可能就存在多个版本的 java.lang.Object类,并且这些类之间是不兼容的。经过代理模式,对于 Java 核心库的类的加载工做由引导类加载器来统一完成,保证了Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不一样的类加载器为相同名称的类建立了额外的名称空间。相同名称的类能够并存在 Java 虚拟机中,只须要用不一样的类加载器来加载它们便可。不一样类加载器加载的类之间是不兼容的,这就至关于在 Java 虚拟机内部建立了一个个相互隔离的 Java 类空间。
测试一:
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader!=null){ System.out.println(loader.toString()); loader = loader.getParent(); } System.out.println(loader); } }
每一个 Java 类都维护着一个指向定义它的类加载器的引用,经过 getClassLoader()方法就能够获取到此引用。代码中经过递归调用 getParent()方法来输出所有的父类加载器。
结果是:
第一个输出的是 ClassLoaderTree
类的类加载器,即系统类加载器。它是sun.misc.Launcher$AppClassLoader
类的实例;第二个输出的是扩展类加载器,是sun.misc.Launcher$ExtClassLoader
类的实例。须要注意的是这里并无输出引导类加载器,这是因为有些 JDK 的实现对于父类加载器是引导类加载器的状况,getParent()方法返回 null。第三行结果说明:ExtClassLoader
的类加器是Bootstrap ClassLoader
,由于Bootstrap ClassLoader
不是一个普通的Java类,因此ExtClassLoader
的parent=null
,因此第三行的打印结果为null就是这个缘由。
测试二:
将ClassLoaderTree.class打包成ClassLoaderTree.jar,放到Extension ClassLoader的加载目录下(JAVA_HOME/jre/lib/ext),而后从新运行这个程序,获得的结果会是什么样呢?
此处我在 IDEA 中的运行结果还和上面的同样,与文章 深刻分析Java ClassLoader原理 中的有差距,具体缘由未弄清楚,还但愿读者可以亲自测试。
那文章中的结果是:
打印结果分析:
为何第一行的结果是ExtClassLoader呢?
由于 ClassLoader 的委托模型机制,当咱们要用 ClassLoaderTest.class 这个类的时候,AppClassLoader 在试图加载以前,先委托给 Bootstrcp ClassLoader,Bootstracp ClassLoader 发现本身没找到,它就告诉 ExtClassLoader,兄弟,我这里没有这个类,你去加载看看,而后 Extension ClassLoader 拿着这个类去它指定的类路径(JAVA_HOME/jre/lib/ext)试图加载,唉,它发如今ClassLoaderTest.jar 这样一个文件中包含 ClassLoaderTest.class 这样的一个文件,而后它把找到的这个类加载到内存当中,并生成这个类的 Class 实例对象,最后把这个实例返回。因此 ClassLoaderTest.class 的类加载器是 ExtClassLoader。
第二行的结果为null,是由于ExtClassLoader的父类加载器是Bootstrap ClassLoader。
隐式加载, 程序在运行过程当中当碰到经过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
显式加载, 经过class.forname()、this.getClass.getClassLoader().loadClass()等方法显式加载须要的类,或者咱们本身实现的 ClassLoader 的 findlass() 方法。
下面介绍下 class.forName的加载类方法:
Class.forName
是一个静态方法,一样能够用来加载类。该方法有两种形式:Class.forName(String name,boolean initialize, ClassLoader loader)和Class.forName(String className)
。第一种形式的参数name
表示的是类的全名;initialize
表示是否初始化类;loader
表示加载时使用的类加载器。第二种形式则至关于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。Class.forName
的一个很常见的用法是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver")
用来加载 Apache Derby 数据库的驱动。
一个应用程序老是由n多个类组成,Java程序启动时,并非一次把全部的类所有加载后再运行,它老是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,由于java最先就是为嵌入式系统而设计的,内存宝贵,这是一种能够理解的机制,而用到时再加载这也是java动态性的一种体现。
第一阶段找到 .class 文件并把这个文件包含的字节码加载到内存中。
第二阶段中分三步,字节码验证;class 类数据结构分析及相应的内存分配;最后的符号表的连接。
第三阶段是类中静态属性和初始化赋值,以及静态块的执行等。
。。
字节码验证,类装入器对于类的字节码要作不少检测,以确保格式正确,行为正确。
类装备,准备表明每一个类中定义的字段、方法和实现接口所必须的数据结构。
解析,装入器装入类所引用的其余全部类。
ClassNotFoundExecption 异常是日常碰到的最多的。这个异常一般发生在显示加载类的时候。
public class ClassNotFoundExceptionTest { public static void main(String[] args) { try { Class.forName("NotFoundClass"); }catch (ClassNotFoundException e){ e.printStackTrace(); } } }
显示加载一个类一般有:
经过类 Class 中的 forName() 方法
经过类 ClassLoader 中的 loadClass() 方法
经过类 ClassLoader 中的 findSystemClass() 方法
出现这种错误其实就是当 JVM 要加载指定文件的字节码到内存时,并无找到这个文件对应的字节码,也就是这个文件并不存在。解决方法就是检查在当前的 classpath 目录下有没有指定的文件。
在JavaDoc中对NoClassDefFoundError的产生可能的状况就是使用new关键字、属性引用某个类、继承了某个接口或者类,以及方法的某个参数中引用了某个类,这时就会触发JVM或者类加载器实例尝试加载类型的定义,可是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是可以被找到的,可是在执行时却没有找到。
解决这个错误的方法就是确保每一个类引用的类都在当前的classpath下面。
该错误一般是在 JVM 启动的时候,若是 JVM 中的某个 lib 删除了,就有可能报这个错误。
public class UnsatisfiedLinkErrorTest { public native void nativeMethod(); static { System.loadLibrary("NoLib"); } public static void main(String[] args) { new UnsatisfiedLinkErrorTest().nativeMethod(); //解析native标识的方法时JVM找不到对应的库文件 } }
该错误一般出现强制类型转换时出现这个错误。
public class ClassCastExceptionTest { public static Map m = new HashMap(){ { put("a", "2"); } }; public static void main(String[] args) { Integer integer = (Integer) m.get("a"); //将m强制转换成Integer类型 System.out.println(integer); } }
注意:JVM 在作类型转换时的规则:
对于普通对象,对象必须是目标类的实例或目标类的子类的实例。若是目标类是接口,那么会把它看成实现了该接口的一个子类。
对于数组类型,目标类必须是数组类型或 java.lang.Object、java.lang.Cloneable、java.io.Serializable。
若是不知足上面的规则,JVM 就会报错,有两种方式可避免错误:
在容器类型中显式的指明这个容器所包含的对象类型。
先经过 instanceof 检查是否是目标类型,而后再进行强制类型的转换。
上面代码中改为以下就能够避免错误了:
public class ExceptionInInitializerErrorTest { public static Map m = new HashMap(){{ m.put("a", "2"); }}; public static void main(String[] args) { Integer integer = (Integer) m.get("a"); System.out.println(integer); } }
在初始化这个类时,给静态属性 m 赋值时出现了异常致使抛出错误 ExceptionInInitializerError。
NoSuchMethodError表明这个类型确实存在,可是一个不正确的版本被加载了。为了解决这个问题咱们可使用 ‘verbose:class’ 来判断该JVM加载的究竟是哪一个版本。
有时候事情会变得更糟,和 ClassCastException 本质同样,加载自不一样位置的相同类在同一段逻辑(好比:方法)中交互时,会出现 LinkageError 。
LinkageError 须要观察哪一个类被不一样的类加载器加载了,在哪一个方法或者调用处发生(交汇)的,而后才能想解决方法,解决方法无外乎两种。第一,仍是不一样的类加载器加载,可是相互再也不交汇影响,这里须要针对发生问题的地方作一些改动,好比更换实现方式,避免出现上述问题;第二,冲突的类须要由一个Parent类加载器进行加载。LinkageError 和ClassCastException 本质是同样的,加载自不一样类加载器的类型,在同一个类的方法或者调用中出现,若是有转型操做那么就会抛 ClassCastException ,若是是直接的方法调用处的参数或者返回值解析,那么就会产生 LinkageError 。
。。参见书籍《深刻分析Java Web技术内幕》
ClassLoader 可以完成的事情有如下状况:
在自定义路径下查找自定义的class类文件。
对咱们本身要加载的类作特殊处理。
能够定义类的实现机制。
虽然在绝大多数状况下,系统默认提供的类加载器实现已经能够知足需求。可是在某些状况下,您仍是须要为应用开发出本身的类加载器。好比您的应用经过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码通过了加密处理。这个时候您就须要本身的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。
定义自已的类加载器分为两步:
一、继承java.lang.ClassLoader
二、重写父类的findClass方法
加载存储在文件系统上的 Java 字节代码。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir){ this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1){ baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
类 FileSystemClassLoader继承自类java.lang.ClassLoader。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;若是没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;若是父类加载器没法加载该类的话,就调用 findClass()方法来查找该类。所以,为了保证类加载器都正确实现代理模式,在开发本身的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文
件),而后读取该文件内容,最后经过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
一个网络类加载器来讲明如何经过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端经过网络的方式获取字节代码并执行。当有版本更新的时候,只须要替换掉服务
器上保存的文件便可。经过类加载器能够比较简单的实现这种需求。
类 NetworkClassLoader 负责经过网络下载 Java 类字节代码并定义出 Java 类。它的实现与FileSystemClassLoader 相似。在经过 NetworkClassLoader 加载了某个版本的类以后,通常有两种作法来使用它。第一种作法是使用 Java 反射 API。另一种作法是使用接口。须要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,由于客户端代码的类加载器找不到这些类。使用 Java 反射 API 能够直接调用 Java 类的
方法。而使用接口的作法则是把接口的类放在客户端中,从服务器上加载实现此接口的不一样版本的类。在客户端经过相同的接口来使用这些实现类。
网络类加载器的代码:ClassLoader
对于运行在 Java EE™容器中的 Web 应用来讲,类加载器的实现方式与通常的 Java 应用有所不一样。不一样的 Web 容器的实现方式也会有所不一样。以 Apache Tomcat 来讲,每一个Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不一样的是它是首先尝试去加载某个类,若是找不到再代理给父类加载器。这与通常类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐作法,其目的是使得Web 应用本身的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围以内的。这也是为了保证 Java 核心库的类型安全。
绝大多数状况下,Web 应用的开发人员不须要考虑与类加载器相关的细节。下面给出几条简单的原则:
每一个 Web 应用本身的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由全部 Web 应用共享的目录下面。
当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确
本篇文章详细深刻的介绍了 ClassLoader 的工做机制,还写了如何本身实现所需的 ClassLoader 。
一、深度分析 Java 的 ClassLoader 机制(源码级别)
五、《深刻分析 Java Web 技术内幕》修订版 —— 深刻分析 ClassLoader 工做机制