用于将Java类加载到Java虚拟机中,不同的类加载器加载的类不可能相等,每一个类,其唯一性都由加载他的类加载器和他本身一同确定,每一个类加载器,都有一个独立的类名称空间,换言之:即使两个类来源同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,这两个类就必定不相等(因此此时会在虚拟机中存在两个同名类,虽然来自一个Class文件,但依然各自独立)。其常用的类加载器有以下三种,当然除此之外还有用户自定义类加载器
C++编写,无法由java程序直接引用。JVM的核心加载器,随Java面世的第一版加载器。我们之所以安装好java,配置好环境变量,运行起java,就可以直接使用Object obj = new Object(),List list = new ArrayList()之类代码,是因为根加载器默认将${JAVA_HOME}/jre/lib/rt.jar加载到虚拟机中。(rt.jar为RunTime的缩写)
Java编写。由于Java诞生后不断的扩展优化,单独使用扩展类加载器加载${JAVA_HOME}/jre/lib/ext目录下的jar,库名通常以javax开头,例如swing等;扩展类库主要是为了兼容旧版本,但某些东西又有了新的解决方案,于是提供扩展类库。
面向程序员的加载器,会加载环境变量中CLASSPATH下的jar,自己生成的类或者第三方的类均有该加载器加载。
List list = new ArrayList();// java包
SwingNode swingNode = new SwingNode();// javax包
Student student = new Student();// 自定义java类
System.out.println(list.getClass().getClassLoader());
System.out.println(swingNode.getClass().getClassLoader());
System.out.println(student.getClass().getClassLoader());
运行结果: 【上述程序分别展示了三种类加载器对不同路径用途的class文件加载,因为根加载器使用C++语言编写,因此在java程序中只能打印出空值】
可以看到三种java文件对应的类加载器,根加载器为null,扩展加载器及应用程序加载器都有一个sun.misc.Launcher前缀,实际上看下图路径,该Launcher类就是JVM的入口。
通过代码体现父类继承关系:
List list = new ArrayList();
SwingNode swingNode = new SwingNode();
Student student = new Student();
System.out.println(swingNode.getClass().getClassLoader().getParent());
System.out.println(student.getClass().getClassLoader().getParent());
System.out.println(student.getClass().getClassLoader().getParent().getParent());
运行结果:
类加载器加载顺序
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的父类加载器,如果父的类加载器没有加载,子类加载器才会尝试去加载。通俗的说就是:不论哪个class文件需要加载,首先给根加载器,若根加载器没找到,则交给扩展类加载器,如果还没找到,才会给应用程序类加载器,再找不到就会报ClassNotFound异常。
ClassLoader为一个抽象类,在虚拟机启动后进行类加载会首先调用loadClassInternal方法,而该方法指向loadClass方法加载class文件,而双亲委派模型就是在该方法中实现。该方法首先判断该类是否已经被加载,若没被加载,则调用parent.loadClass方法加载(即父加载器),parent.loadClass方法中依然会判断parent是否为null,若依然有父类加载器,则继续递归向上调用,直至parent==null。当其父加载器抛出ClassNotFoundException时,说明没找到,则由子加载器继续加载,符合双亲委派机制。
public abstract class ClassLoader {
private final ClassLoader parent;
// 该方法在虚拟机启动进行类加载时会立刻调用
private Class> loadClassInternal(String name)
throws ClassNotFoundException
{
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
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;
}
}
}
好处
保证源代码不受污染。假设你自己定义了一个java.lang.String类,去运行,若没有此机制,会污染原本jdk中带的String类,导致代码出现不可预知的问题,程序将变得一片混乱。
在Java诞生以来,历史上有三次大规模破坏:
第一次:双亲委派模型为JDK1.2推出,但在此之前就已经存在了ClassLoader,因此需要兼容。上述ClassLoader源码在loadClass方法中实现的双亲委派,因此如果想破坏最直接的办法就是继承ClassLoader并重写loadClass方法,该方法无疑是破坏性的,但因为JDK1.2以前都是这么做的,该接口无奈保留支持重写,但新的代码绝对不推荐如此使用。
ClassLoader在JDK1.2之后新增了一个findClass方法,目的就是当父类加载器加载失败时,将子类加载器的逻辑写在findClass中完成自定义设计。
第二次:模型自身缺陷导致。可见3.3,父加载器不能访问子加载器加载的类,因此当核心类库的Java需要调用自定义类的Java文件时,将加载失败。因此Java团队引入了线程上下文类加载器(Thread Context ClassLoader),该加载器可通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还没设置,则会从父线程中继承,如果父线程也没设置,那默认就是AppClassLoader。
第三次:代码热部署。我们总是希望修改了java类内容不重启JVM就可以看到他的改变,但默认的机制并不支持。想要支持需要引入第三方jar,该jar不再采用双亲委派模型。
预加载:JVM启动时,会预先加载一部分核心类库,包括:Object、String、Thread、Integer、Double、ClassLoader等核心到不能再核心的,由于其使用频率非常高,与其之后用到再加载,还不如启动JVM时直接加载内存以便使用,当然这些类不能过多,会降低JVM的启动速度。其中预加载ClassLoader可初始化扩展类加载器和应用程序类加载器。
延迟加载:除去上述预加载的java类,其余的无论是JDK类库还是其他类库的,都采用延迟加载。即使在程序中使用import引入了该类,只要不真正使用该类,JVM就不会进行加载,降低了系统开销。
所有Class被加载后,均会被缓存,每次需要时预先从缓存中寻找,若不存在,再加载该Class,因此每次修改了Java文件,都必须重启JVM,刷新缓存。
当某个类加载器负责加载Class时,该Class中依赖和引用的所有其他Class也将由该类加载器加载,除非显式的手动使用另一个类加载器强行加载,由2.1的源码分析可知,调用该类加载器的loadClass方法可加载范围为 自己+父类加载器,因此:
ClassLoader相当于类的命名空间,起到了类隔离的作用。每一个类,其唯一性都由加载他的类加载器和他本身一同确定,每一个类加载器,都有一个独立的类名称空间,位于同一个ClassLoader里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader是类名称的容器,是类的沙箱。
作为一个主流的Java Web服务器,其应该具有以下几个特征:
基于以上目的,Tomcat团队规划了类库结构和加载器:
在Tomcat5.x以前的版本,Tomcat目录有4个:
为了支持这套目录结构,并对目录里的类库进行加载隔离(隔离技术请看3.4),Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,关系图如下:
由于其层级关系:
在Tomcat6.x及以后版本,默认将/common、/server、/shared三个文件夹合并为/lib,这是Tomcat设计团队为简化大多数部署场景做的一项改进,若默认设置不能满足需求,可通过在tomcat/catalina/properties配置文件的server.loader和shared.loader配置项建立CatalinaClassLoader和SharedClassLoader实例,重新启用Tomcat5.x的加载器架构。