JVM(六)虚拟机类加载委派模型

双亲委派模型

当一个类装载器(class loader)被请求装载类时,它首先按照顺序在上层装载器、父装载器以及自身的装载器的缓存里检查这个类是否已经存在。简单来说,就是在缓存里查看这个类是否已经被自己装载过了,如果没有的话,继续查找父类的缓存,直到在 bootstrap 类装载器里也没有找到的话,它就会自己在文件系统里去查找并且加载这个类。
ClassLoader 的 loadClass 方法

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;
    }
}

1)先找缓存(findLoadedClass),没有的话就判断有没有 parent,有的话就用 parent 来递归的 loadClass,然而 ExtClassLoader 并没有设置 parent,则会通过 findBootstrapClassOrNull 来加载 class,而 findBootstrapClassOrNull 则会通过 JNI 方法“private native Class findBootstrapClass(String name)“来使用 BootStrapClassLoader 来加载 class。
2)然后如果 parent 未找到 class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法,可以覆盖它以便自定义 class 加载过程。另外,虽然 ClassLoader 加载类是使用 loadClass 方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写 loadClass,这样就不会覆盖了类加载默认的双亲委派机制。
双亲委派模型有一个缺陷,如果父 ClassLoader 想加载子 ClassLoader 中的类比较困难,而在有的应用中这种加载方式是需要的,比如 JNDI,Servlet 等。

破坏双亲委派模型

1) JDK1.2 java.lang.ClassLoader 提供新的 protected 方法 findClass()和 public 方法 loadClass(),来实现自定义类加载和破坏委派模型。
2)JDK1.3 使用线程上下文(ThreadContextClassLoader)解决让父类加载器请求子类完成类的加载,例如 JNDI,JDBC,JCE,JAXB,以及应用服务器 Tocmat 等
3)OSGi,代码热替换(HotSwap),代码热部署(HotDeployment)
关于 JDBC 的 SPI 加载,可参照 ServiceLoader 深入解析,本文主要讲解下 jetty 的类加载机制。
Jetty 的 ClassLoader 体系结构如下所示:
image.png
Jetty 有两种运行方式,一种进程内运行(通过反射执行 MainClass),一种是进程外执行 ( 通过 Runtime.getRuntime().exec()), 进程外执行时由于线程上下文不能进程之间传递。
Jetty 至少要保证其内部运行的多个 webapp 之间私有的类库不受影响,并且公有的类库可以共享。Jetty 中有一个 org.mortbay.jetty.webapp.WebAppClassLoader,负责加载一个 webapp context 中的应用类,WebAppClassLoader 以系统类加载器作为 parent,用于加载系统类。不过 servlet 规范使得 web 容器的 ClassLoader 比正常的 ClassLoader 委托模型稍稍复杂,Servlet 规范要求:
1)WEB-INF/lib 和 WEB-INF/classes 优先于父容器中的类加载,比如 WEB-INF/classes 下有个 XYZ 类,classpath 下也有个 XYZ 类,jetty 中优先加载的是 WEB-INF/classes 下的,这与正常的父加载器优先相反(child-first)。
2)系统类比如 java.lang.String 不遵循第一条,WEB-INF/classes 或 WEB-INF/lib 下的类不能替换系统类。不过规范中没有明确规定哪些是系统类,jetty 中的实现是按照类的全路径名判断。
3)Server 的实现类不被应用中的类引用,即 Server 的实现类不能被任何应用类加载器加载。不过,同样的,规范里没有明确规定哪些是 Server 的实现类,jetty 中同样是按照类的全路径名判断。
为了处理上述三个问题,jetty 的应用类加载器(org.mortbay.jetty.webapp.WebAppClassLoader)做了些特殊处理。

public WebAppClassLoader(ClassLoader parent, Context context)
        throws IOException {
    super(new URL[] {}, parent != null ? parent
            : (Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader()
            : (WebAppClassLoader.class.getClassLoader() != null ? WebAppClassLoader.class.getClassLoader()
            : ClassLoader.getSystemClassLoader())));
    _parent = getParent();
    _context = context;
    if (_parent == null)
        throw new IllegalArgumentException("no parent classloader!");

    _extensions.add(".jar");
    _extensions.add(".zip");

    // TODO remove this system property
    String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
    if (extensions != null) {
        StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
        while (tokenizer.hasMoreTokens())
            _extensions.add(tokenizer.nextToken().trim());
    }
    if (context.getExtraClasspath() != null)
        addClassPath(context.getExtraClasspath());
}

他是以当前线程上下文的 ClassLoader 为父 classloader, 如果上下文没有设定 classLoader ,就使用加载 WebAppClassLoader 的加载器,如果还是没有,则采用系统类加载器。很明显,默认情况下,如果采用进程内运行,那么这个 parent 就是 Loader( 系统自定义类加载器 ) ,如果是进程外, parent 就是系统类加载器。
WebAppClassLoader 可以设定是否由 parent 优先加载 lib/ 、 classes 下的类。

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> c = findLoadedClass(name);
    ClassNotFoundException ex = null;
    boolean tried_parent = false;

    boolean system_class = _context.isSystemClass(name);
    boolean server_class = _context.isServerClass(name);

    if (system_class && server_class) {
        return null;
    }

    if (c == null && _parent != null && (_context.isParentLoaderPriority() || system_class) && !server_class) {
        tried_parent = true;
        try {
            c = _parent.loadClass(name);
            if (LOG.isDebugEnabled())
                LOG.debug("loaded " + c);
        }
        catch (ClassNotFoundException e) {
            ex = e;
        }
    }

    if (c == null) {
        try {
            c = this.findClass(name);
        }
        catch (ClassNotFoundException e) {
            ex = e;
        }
    }

    if (c == null && _parent != null && !tried_parent && !server_class)
        c = _parent.loadClass(name);

    if (c == null && ex != null)
        throw ex;

    if (resolve)
        resolveClass(c);

    if (LOG.isDebugEnabled())
        LOG.debug("loaded {} from {}", c, c == null ? null : c.getClassLoader());

    return c;
}

findLoadedClass(name)-检查类是否已经加载
1)判断该类是否为系统类或 server 类
2)如果该类未加载且父加载器不为空且设置了父加载器优先或类类为系统类,且该类不是 server 类,则尝试使用父加载器加载该类
3)如果不是父加载器优先或者父加载器未加载到该类,使用 WebAppClassLoader 加载该类
4)如果是不是父加载器优先,并且 WebAppClassLoader 未加载到该类,尝试使用父加载器加载该类
5)找到则返回,否则抛出 ClassNotFoundException
再看下 findClass 方法

protected Class<?> findClass(final String name) throws ClassNotFoundException {
    Class<?> clazz = null;

    if (_transformers.isEmpty())
        clazz = super.findClass(name);
    else {
        String path = name.replace('.', '/').concat(".class");
        URL url = getResource(path);
        if (url == null)
            throw new ClassNotFoundException(name);

        InputStream content = null;
        try {
            content = url.openStream();
            byte[] bytes = IO.readBytes(content);

            for (ClassFileTransformer transformer : _transformers) {
                byte[] tmp = transformer.transform(this, name, null, null, bytes);
                if (tmp != null)
                    bytes = tmp;
            }

            clazz = defineClass(name, bytes, 0, bytes.length);
        }
        catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
        catch (IllegalClassFormatException e) {
            throw new ClassNotFoundException(name, e);
        }
        finally {
            if (content != null) {
                try {
                    content.close();
                }
                catch (IOException e) {
                    throw new ClassNotFoundException(name, e);
                }
            }
        }
    }

    return clazz;
}

_transformers 为空调用父类的 findClass,不为空找到对应的类文件对其进行转换。

1、设置 ClassLoader Priority

上述过程涉及一个加载器优先级的概念,这也是针对前述第一条规范中 WEB-INF/lib 和 WEB-INF/classes 类优先的处理。jetty 中父加载器优先的配置项可以通过环境变量
org.eclipse.jetty.server.webapp.parentLoaderPriority=false(默认)/true 来设置
也可以通过
org.eclipse.jetty.webapp.WebAppContext.setParentLoaderPriority(boolean)方法来设置
优于该配置默认是 false,因此在 load class 过程中优先使用 WebAppClassLoader 加载 WEB-INF/lib 和 WEB-INF/classes 中的类。当将该配置项设为 true 时需要确认类加载顺序没有问题

2、设置系统类

规范 2 中约定系统类不能被应用类覆盖,但是没有明确规定哪些时系统类,jetty 中以类的 package 路径名来区分,当类的 package 路径名位包含于

// System classes are classes that cannot be replaced by
// the web application, and they are *always* loaded via
// system classloader.
public final static String[] __dftSystemClasses =
{
       "java.",                            // Java SE classes (per servlet spec v2.5 / SRV.9.7.2)
       "javax.",                           // Java SE classes (per servlet spec v2.5 / SRV.9.7.2)
       "org.xml.",                         // needed by javax.xml
       "org.w3c.",                         // needed by javax.xml
       "org.eclipse.jetty.jmx.",           // webapp cannot change jmx classes
       "org.eclipse.jetty.util.annotation.",  // webapp cannot change jmx annotations
       "org.eclipse.jetty.continuation.",  // webapp cannot change continuation classes
       "org.eclipse.jetty.jndi.",          // webapp cannot change naming classes
       "org.eclipse.jetty.jaas.",          // webapp cannot change jaas classes
       "org.eclipse.jetty.websocket.",     // webapp cannot change / replace websocket classes
       "org.eclipse.jetty.util.log.",      // webapp should use server log
       "org.eclipse.jetty.servlet.ServletContextHandler.Decorator", // for CDI / weld use
       "org.eclipse.jetty.servlet.DefaultServlet", // webapp cannot change default servlets
       "org.eclipse.jetty.jsp.JettyJspServlet", //webapp cannot change jetty jsp servlet
       "org.eclipse.jetty.servlets.AsyncGzipFilter" // special case for AsyncGzipFilter
} ;

时,会被认为是系统类。(该定义位于 WebAppContext 中)
因此,我们可以通过 org.eclipse.jetty.webapp.WebAppContext.setSystemClasses(String Array)或者 org.eclipse.jetty.webapp.WebAppContext.addSystemClass(String)来设置系统类。 再次提醒,系统类是对多有应用都可见。

3、设置 Server 类

规范 3 中约定 Server 类不对任何应用可见。jetty 同样是用 package 路径名来区分哪些是 Server 类。Server 类包括:

// Server classes are classes that are hidden from being
// loaded by the web application using system classloader,
// so if web application needs to load any of such classes,
// it has to include them in its distribution.
public final static String[] __dftServerClasses =
{
       "-org.eclipse.jetty.jmx.",          // don't hide jmx classes
       "-org.eclipse.jetty.util.annotation.", // don't hide jmx annotation
       "-org.eclipse.jetty.continuation.", // don't hide continuation classes
       "-org.eclipse.jetty.jndi.",         // don't hide naming classes
       "-org.eclipse.jetty.jaas.",         // don't hide jaas classes
       "-org.eclipse.jetty.servlets.",     // don't hide jetty servlets
       "-org.eclipse.jetty.servlet.DefaultServlet", // don't hide default servlet
       "-org.eclipse.jetty.jsp.",          //don't hide jsp servlet
       "-org.eclipse.jetty.servlet.listener.", // don't hide useful listeners
       "-org.eclipse.jetty.websocket.",    // don't hide websocket classes from webapps (allow webapp to use ones from system classloader)
       "-org.eclipse.jetty.apache.",       // don't hide jetty apache impls
       "-org.eclipse.jetty.util.log.",     // don't hide server log
       "-org.eclipse.jetty.servlet.ServletContextHandler.Decorator", // don't hide CDI / weld interface
       "org.objectweb.asm.",               // hide asm used by jetty
       "org.eclipse.jdt.",                 // hide jdt used by jetty
       "org.eclipse.jetty."                // hide other jetty classes
} ;

我们可以通过, org.eclipse.jetty.webapp.WebAppContext.setServerClasses(String Array) 或 org.eclipse.jetty.webapp.WebAppContext.addServerClass(String)方法设置 Server 类。 注意,Server 类是对所有应用都不可见的,但是 WEB-INF/lib 下的类可以替换 Server 类。
当默认的 WebAppClassLoader 不能满足需求时,可以自定义 WebApp ClassLoader,不过 jetty 建议自定义的 classloader 要扩展于默认的 WebAppClassLoader 实现。

https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. 双亲委派模型
  2. 破坏双亲委派模型
    1. 1、设置 ClassLoader Priority
    2. 2、设置系统类
    3. 3、设置 Server 类