双亲委派机制
什么是双亲委派机制?
一个类加载器收到了类加载的请求, 它首先不会自己去尝试自己去加载这个类,而是吧这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(即搜索范围中没有找到所需的类)时,子加载器才会尝试自己完成加载。
类加载和双亲委派模型如下图所示
我们再来看看 ClassLoader
类的 loadClass
方法
// 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) { // 如果父类类加载器不为空,先尝试父类加载来加载 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; } } // 类加载器的包含关系 public abstract class ClassLoader { private static native void registerNatives(); static { registerNatives(); } // 当前 ClassLoader 和 parent ClassLoader 的包含关系 private final ClassLoader parent; }
总结:
- 不是树形结构(只是逻辑树形结构),而是包含/包装关系。
- 加载顺序,应用类加载器,拓展加载器,系统加载器。
- 如果有一个类加载器能够成功加载
Test
类,那么这个类加载器被称为定义类加载器,所有可能返回 Class 对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。
设计双亲委派机制的目的?
- 保证 Java 核心库的类型安全:所有的java 应用都会至少引用
java.lang.Object
类, 也就是说在运行期, java.lang.Object 的这个类会被加载到 Java 虚拟机中,如果这个加载过程是由 Java 应用自己的类加载器所完成的,那么很有可能会在 JVM 中存在多个版本的 java.lang.Object 类,而且这些类之间还是不兼容的。互不可见的(正是命名空间发挥着作用)借助于双亲委托机制,Java 核心库中的类加载工作都是由启动类加载器统一来完成的。从而确保了Java 应用所使用的都是同一个版本的 Java 核心类库,他们之间是相互兼容的。
- 可以确保 Java 核心库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同类(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要不同的类加载器来加载他们即可,不同的类加载器的类之间是不兼容的,这相当于在JAVA虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际运用。
自定义类加载器
自定义类加载器加载类,下面是一个简单的 Demo
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class ClassLoaderTest extends ClassLoader { private static String rxRootPath; static { rxRootPath = "/temp/class/"; } @Override public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } /** * 读取 .class 文件为字节数组 * * @param name 全路径类名 * @return */ private byte[] loadClassData(String name) { try { String filePath = fullClassName2FilePath(name); InputStream is = new FileInputStream(new File(filePath)); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[2048]; int r; while ((r = is.read(buf)) != -1) { bos.write(buf, 0, r); } return bos.toByteArray(); } catch (Throwable e) { e.printStackTrace(); } return null; } /** * 全限定名转换为文件路径 * * @param name * @return */ private String fullClassName2FilePath(String name) { return rxRootPath + name.replace(".", "//") + ".class"; } public static void main(String[] args) throws ClassNotFoundException { ClassLoaderTest classLoader = new ClassLoaderTest(); String className = "com.test.TestAA"; Class clazz = classLoader.loadClass(className); System.out.println(clazz.getClassLoader()); // 输出结果 //cn.xxx.xxx.loader.ClassLoaderTest@3764951d } }
Tomcat 类加载器
Tomcat 中的类加载器模型
Tomcat 类加载器说明
tomcat 的几个主要类加载器:
- commonLoader: Tomcat 最基本的类加载器, 加载路径中的 class 可以被 Tomcat 容器本身以及各个 WebApp 访问。
- catalinaLoader:Tomcat 容器私有的类加载器 加载路径中的 class 对于 Webapp 不可见;
- sharaLoader: 各个Webapp 共享的类加载器, 加载路径中的 class 对于所有 webapp 可见, 但是对于 Tomcat 容器不可见。
- webappLoader: 各个 Webapp 私有的类加载, 加载路径中的 class 只对当前 webapp 可见, 比如加载 war 包里面相关的类,每个 war 包应用都有自己的 webappClassLoader 对象,对应不同的命名空间,实现相互隔离,比如 war 包中可以引入不同的 spring 版本,实现多个 spring 版本 应用的同时运行。
总结:
从图中的委派关系中可以看出:
Commonclassloader 能加载的类都可以被 Catalinaclassloader和 Sharedclassloadert 使用, 从而实现了公有类库的共用,而Catalinaclassloader 和 Sharedclassloader自己能加载的类则与对方相互隔离 Webappclassloader 可以使用 Shared Loader 加载到的类,但各个 Webappclassloader 实例之间相互隔离而 Jasper Loader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 . class 文件,它出现的目的就是为了被丢弃: 当 Web 容器检测到 JSP 文件被修改时,会替换掉目前的 Jasperloader 的实例,并通过再建立一个新的 JSP 类加载器来实现 JSP 文件的热加载功能。
Tomcat 这种类加载机制打破了Java 推荐的双亲委派模型了吗? 答案是: 打破了
Tomcat 不是这样实现, Tomcat 为了实现隔离性, 没有遵守这个约定, 每个 webapp Loader加载自己的目录下的 class 文件, 不会传递给父类加载器,打破了双亲委派机制
参考资料
- 《深入理解 Java 虚拟机》 第三版 周志明