JVM双亲委派模型和破坏双亲委派

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是一种优先级关系。

这样在类加载的时候,首先会把该请求委派给该类加载器的父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。

类加载流程:

  • 子类先委托父类加载
  • 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
  • 子类在收到父类无法加载的时候,才会自己去加载

jvm提供了三种系统加载器:

  • 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载jdk核心类库。
  • 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar
  • 系统类加载器/应用程序类加载器(Application ClassLoader):我们写的代码默认就是由它来加载。负责加载应用程序classpath目录下的所有jar和class文件。
    JVM双亲委派模型和破坏双亲委派_第1张图片
    除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,则需要继承 ClassLoader,重写loadClass()方法。
    因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。

双亲委派的好处

1、双亲委派模型实际是将类加载器加载类的过程加上了优先级次序,通过这种层级关可以避免类的重复加载,当父亲已经加载了某类时,就没有必要子类加载器再加载一次。
(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)

2、是考虑到安全因素,java核心api不会被篡改。比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。而双亲委派模式下,java核心api 被加载的优先级更高,且只加载一次,应用程序加载时只能直接使用。

如果不想使用双亲委托机制,我们可以自己定义一个类加载器,然后重写 loadClass() 即可。

双亲委派模型实现源码

private final ClassLoader parent; 
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) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }
                
                if (c == null) {
                    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;
        }
    }

破坏双亲委派

  • 第一次“被破坏”,是在引入双亲委派模型时(jdk1.2),为了兼容之前的版本。因为jdk1.0 时就可以用户自定义类加载器,引入双亲委派模型时不得不做出一些妥协。为了能把用户自定义的类加载器纳入双亲委派的优先级层次中,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass()。JDK1.2之后不再提倡用户再去覆盖loadClass()方法自定义类加载器,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
  • 第二次“被破坏”是因为这个模型自身的缺陷。双亲委派很好地解决了类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但如果基础类又要调用回用户的代码呢?
    一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
    为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。
  • 双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
    OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

推荐阅读

  • https://blog.csdn.net/xyang81/article/details/7292380
  • https://juejin.im/post/5c04892351882516e70dcc9b
  • http://gityuan.com/2016/01/24/java-classloader/

你可能感兴趣的:(其他,java编程)