jvm双亲委派模型

其实,双亲委派模型并不复杂。自定义类加载器也不难!随便从网上搜一下就能搜出一大把结果,而后copy一下就能用。可是,若是每次想自定义类加载器就必须搜一遍别人的文章,而后复制,这样显然不行。但是自定义类加载器又不常常用,时间久了容易忘记。相信你常常会记不太清loadClassfindClassdefineClass这些函数我到底应该重写哪个?它们主要是作什么的?本文大体分析了各个函数的流程,目的就是让你看完以后,难以忘记!或者说,延长你对自定义类加载器的记忆时间!随时随地想自定义就自定义!java

1. 双亲委派模型

关于双亲委派模型,网上的资料有不少。我这里只简单的描述一下,就当是复习。apache

1.1 什么是双亲委派模型?

首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。若是站在JVM的角度来看,只存在两种类加载器:bootstrap

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。数组

  • 其余类加载器:由Java语言实现,继承自抽象类ClassLoader。如:app

    • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的全部类库。
    • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,咱们能够直接使用这个类加载器。通常状况,若是咱们没有自定义类加载器默认就是用这个加载器。

双亲委派模型工做过程是:若是一个类加载器收到类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器完成。每一个类加载器都是如此,只有当父加载器在本身的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试本身去加载。框架


类加载器的双亲委派模型

1.2 为何须要双亲委派模型?

为何须要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:ide

黑客自定义一个java.lang.String类,该String类具备系统的String类同样的功能,只是在某个函数稍做修改。好比equals函数,这个函数常用,若是在这这个函数中,黑客加入一些“病毒代码”。而且经过自定义类加载器加入到JVM中。此时,若是没有双亲委派模型,那么JVM就可能误觉得黑客自定义的java.lang.String类是系统的String类,致使“病毒代码”被执行。函数

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。由于首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器没法加载java.lang.String类。ui

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去经过调用父加载器不就行了吗?确实,这样是可行。可是,在JVM中,判断一个对象是不是某个类型时,若是该对象的实际类型与待比较的类型的类加载器不一样,那么会返回false。this

举个简单例子:

ClassLoader1ClassLoader2都加载java.lang.String类,对应Class一、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

1.3 如何实现双亲委派模型?

双亲委派模型的原理很简单,实现也简单。每次经过先委托父类加载器加载,当父类加载器没法加载时,再本身加载。其实ClassLoader类默认的loadClass方法已经帮咱们写好了,咱们无需去写。

2. 自定义类加载器

2. 1几个重要函数

2.1.1 loadClass

loadClass默认实现以下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

再看看loadClass(String name, boolean resolve)函数:

复制代码
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;
    }
}
复制代码

从上面代码能够明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大体过程以下:

  1. 首先,检查一下指定名称的类是否已经加载过,若是加载过了,就不须要再加载,直接返回。
  2. 若是此类没有加载过,那么,再判断一下是否有父加载器;若是有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  3. 若是父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

话句话说,若是自定义类加载器,就必须重写findClass方法!

2.1.1 find Class

findClass的默认实现以下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

能够看出,抽象类ClassLoaderfindClass函数默认是抛出异常的。而前面咱们知道,loadClass在父加载器没法加载类的时候,就会调用咱们自定义的类加载器中的findeClass函数,所以咱们必需要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.

若是是是读取一个指定的名称的类为字节数组的话,这很好办。可是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,经过这个方法,就能够把一个字节数组转为Class对象啦~

2.1.1 defineClass

defineClass主要的功能是:

将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则须要解密后做为形参传入defineClass函数。

defineClass默认实现以下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

 

双亲模式的问题

顶层ClassLoader,没法加载底层ClassLoader的类

Java框架(rt.jar)如何加载应用的类?

好比:javax.xml.parsers包中定义了xml解析的类接口
Service Provider Interface SPI 位于rt.jar
即接口在启动ClassLoader中。
而SPI的实现类,在AppLoader。

这样就没法用BootstrapClassLoader去加载SPI的实现类。

 

解决

JDK中提供了一个方法:

1:  Thread. setContextClassLoader()

用以解决顶层ClassLoader没法访问底层ClassLoader的类的问题;
基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例。

 

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。若是没有经过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器(appClassLoader)。在线程中运行的代码能够经过此类加载器来加载类和资源。

  前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的所有问题。Java 提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码极可能是做为 Java 应用所依赖的 jar 包被包含进来,能够经过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码常常须要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类通常是由系统类加载器来加载的。引导类加载器是没法找到 SPI 的实现类的,由于它只加载 Java 的核心库。它也不能代理给系统类加载器,由于它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式没法解决这个问题。

  线程上下文类加载器正好解决了这个问题。若是不作任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就能够成功的加载到 SPI 实现的类。线程上下文类加载器在不少 SPI 的实现中都会用到。

JNDI,JDBC的诉求是:

  为了能让应用程序访问到这些jar包中的实现类,即用appClassLoarder去加载这些实现类。能够用getContextClassLoader取得当前线程的ClassLoader(即appClassLoarder),而后去加载这些实现类,就能让应用访问到

相关文章
相关标签/搜索