类加载过程分为加载、连接、初始化三个阶段,而加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。
Java特意把这一步抽出来用类加载器来实现。把这一步骤抽离出来使得应用程序可以按需自定义类加载器。并且得益于类加载器,OSGI、热部署等领域才得以在JAVA中得到应用。
类加载器
在Java中任意一个类都是由这个Class本身和加载这个类的ClassLoader来确定这个类在JVM中的唯一性。也就是你用你ClassLoadserA加载的ClassA和ClassLoadserB加载的ClassA是不同的,也就是用instanceof这种对比都是不同的。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。
类加载器的层次划分:
- 启动类加载器(Bootstrap ClassLoader):它是属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它等于是所有类加载器的父类加载器。
- 扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。
- 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它是Java实现的,独立于虚拟机,它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由上述这三种类加载器互相配合从而实现类加载,如果有必要,还可以加入自己定义的类加载器。
双亲委派模型
一言概之,双亲委派模型,其实就是一种类加载器的层次关系。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
1、工作过程
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
2、双亲委派模型优势
- 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,同时也避免了类的重复加载问题。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
- 防止核心API库被随意篡改。考虑到安全因素,Java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class
3、代码实现:java.lang.ClassLoader
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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; } } |
工作流程,可以用下面一张图解释:
主要可以分为两步:
- 首先从底向上的检查类是否已经加载过,就是这行代码
Java
1 2 | // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); |
- 如果都没有加载过的话,那么就自顶向下的尝试加载该类。
总结
双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错,它是JAVA设计者推荐使用的一种类加载器的方式。
但是有些情况不得不违反这个约束,例如JDBC就破坏了双亲委派模型。
Java里的SPI(Service Provider Interface),就明显违反了双亲委派模型的约束和规范,JDBC也是基于SPI实现的,在rt里面定义了SPI,由Mysql/Oracle等驱动分别实现,Bootstrap ClassLoader就得委托子类加载器来加载数据库厂商们提供的具体实现。因为只能加载到<JAVA_HOME>\lib中,其他的则无能为力。这就违反了自下而上的委托机制了。
转载请注明:刘召考的博客 » Java类加载的双亲委派模型–看这一篇足够了