classloader详解

本文主要包含下面几个内容:

  1. classloader双亲委派机制以及classloader加载class的流程
  2. classloader的其他特性
  3. 自定义classloader以及如何打破双亲委派机制
  4. context classloader作用

classloader双亲委派机制以及classloader加载class的流程

java类加载流程

JVM启动时,有三个classloader负责加载class,如下:

  • bootstrap classloader
  • extension classloader
  • system classloader
  1. bootstrap classloader:采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类; 这些类位于$JAVA_HOME/jre/lib/下面。当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
  2. extension classloader:扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar。
  3. system classloader: 系统class loader,父类是ExtClassLoader,加载$CLASSPATH下的目录和jar;它负责加载应用程序主函数类。

为了更好的理解,直接查看源码,省略了非关键代码。sun.misc.Launcher, 它是java程序的入口。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

    static class ExtClassLoader extends URLClassLoader {
      //...
    }


    static class AppClassLoader extends URLClassLoader {
      //...
    }

  //...
}

bootstrap classloader负责加载Launcher类,其中代码里面的bootClassPath为bootstrap classloader的加载路径,获取sun.boot.class.path属性为$JAVA_HOME/jre/lib/下面jar拼接成的,如下:

D:\java_tools\java\jdk8\jre\lib\resources.jar
D:\java_tools\java\jdk8\jre\lib\rt.jar
D:\java_tools\java\jdk8\jre\lib\sunrsasign.jar
D:\java_tools\java\jdk8\jre\lib\jsse.jar
D:\java_tools\java\jdk8\jre\lib\jce.jar
D:\java_tools\java\jdk8\jre\lib\charsets.jar
D:\java_tools\java\jdk8\jre\lib\jfr.jar
D:\java_tools\java\jdk8\jre\classes

同时在代码里面构造了ExtClassLoader和AppClassLoader,两者都继承了URLClassLoader,其中ExtClassLoader的parent为null(其中为null表示parent为bootstrap classloader),URLs为System.getProperty("java.ext.dirs"), 值为$JAVA_HOME/jre/lib/ext,具体的代码在var1 = Launcher.ExtClassLoader.getExtClassLoader();

另外AppClassLoader的parent为ExtClassLoader,URLs为System.getProperty("java.class.path")获取的值,值为-classpath传递的值, 具体的代码在this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
所以classloader的继承关系如下:

+-BootstrapClassLoader [bootstrap classloader]                                                                                                                                                                                  
  +-sun.misc.Launcher$ExtClassLoader@7bf2dede [extension classloader]                                                                                                                            
    +-sun.misc.Launcher$AppClassLoader@18b4aac2  [system classloader]       

双亲委派机制

ExtClassLoader和AppClassLoader都继承了URLClassLoader, URLClassLoader又继承了ClassLoader类,类加载器在加载类的时候,最终会调用ClassLoader类的loadClass方法,正是该方法决定了类的加载机制是双亲委派,源码如下:

public abstract class ClassLoader {
//...

    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. 根据类名尝试从本地缓存里面获取已经加载的class,如果没有转2,如果有转最后一步。
  2. 判断parent是否为null:不为null,直接使用parent的classloader加载;为null,相当于parent是bootstrap classloader,使用bootstrap classloader加载。如果没有转3,如果有转最后一步。
  3. 调用findClass方法,根据一定的路径策略获取class,没有找到的话返回null,找到转最后一步。
  4. 解析class。

上面的几个步骤可以看到,优先由parent的classloader加载,这就是双亲委派机制。其中第3步可以覆盖 findClass方法,实现自己的加载策略:比如可以从远程网络class文件,从本地压缩包里面获取class文件等。

classloader的其他特性

除了双亲委派特性,classloader还有隐式加载,隔离等特性。

隐式加载

JVM加载class文件到内存有两种方式。

  1. 隐式加载:所谓隐式加载就是不通过在代码里调用classloader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中继承或者引用某个类时,JVM在解析当前这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
  2. 显式加载:相反的显式加载就是我们在代码中通过调用classloader类来加载一个类的方式,调用this.getClass.getClassLoader().loadClass()或者Class.forName(),或者我们自己实现的ClassLoader的findClass()方法等。

其实这两种方式是混合使用的,例如,我们通过自定义的classloader显式加载一个类时,这个类中又引用了其他类,那么这些类就是隐式加载的。正如所有程序都有一个main函数一样,所有的应用都有一个或多个入口的类,这个类是被最先加载的,并且随后的所有类都像树枝一样以此类为根被加载。

举两个例子:

  1. java程序运行的时候,都会首先从拥有main方法的入口类运行,该类由AppClassLoader加载,从而其他被它应用的类都会由AppClassLoader来加载。
  2. springboot应用的启动方式,基于springboot的应用,最终会被打成一个jar包的形式运行,jar包的META-INF/MANIFEST.MF文件里面指定Main-Class以及其他相关关键信息如下:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.dada.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

Main-Class为org.springframework.boot.loader.JarLauncher, jar包启动的时候会首先执行JarLauncher的main方法。大致的逻辑是:在JarLauncher里面使用自定义的LaunchedURLClassLoader(parent为system ClassLoader)加载真实的Main-Class,对应上面的Start-Class,关键源码如下:

package org.springframework.boot.loader;

import java.lang.reflect.*;

public class MainMethodRunner
{
    // 省略非关键代码
    public void run() throws Exception {
        // this.mainClassName 对应的就是上面META-INF/MANIFEST.MF里面的Start-Class属性
        final Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
        final Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, this.args);
    }
}

其中Thread.currentThread().getContextClassLoader()获取的就是LaunchedURLClassLoader(在前面设置,具体可以参考org.springframework.boot.loader.Launcher#launch方法),通过显示加载的方式加载Start-Class:com.dada.Application (即真正的应用Main-Class),也就是应用的入口类,该入口类会让其他被它引用的类使用LaunchedURLClassLoader进行加载。

隔离性

为了理解隔离性,需要先理解下面几个概念

  1. 不同的classloader加载的同一个class文件,会被jvm认为是不同的class。如果把一个ClassLoader创建的实例,赋值给另一个ClassLoader加载的类,会导致ClassCastException异常。
  2. class冲突,同一个classloader只能加载一个class name(包括package)的class,如果存在多个class name相同的类,会出现随机加载class,从而导致NoSuchMethodError等异常。
  1. 两个平级的classloader加载的两个类,不能相互访问,比如在下面的场景:
+-BootstrapClassLoader [bootstrap classloader]                                                                                                                                                                                  
  +-sun.misc.Launcher$ExtClassLoader@7bf2dede [extension classloader]                                                                                                                            
    +-sun.misc.Launcher$AppClassLoader@18b4aac2  [system classloader] 
       +-自定义的classloader1
       +-自定义的classloader2

其中classloader1加载的class不能访问classloader2加载的class。

  1. 一个classloader可以访问父classloader加载的class,比如自定义的classloader1可以访问AppClassLoader加载的类。这是双亲委派机制决定的。
  2. 父classloader加载的class不能访问子classloader加载的class,比如AppClassLoader不能访问自定义的classloader1加载的类。这也是双亲委派机制决定的。

举个例子:
一个tomcat可以同时启动多个不同的webapp(基于springmvc),多个不同的webapp可能拥有完全相同的类,那么是如何保证不会出现class冲突?正是使用了不同classloader的隔离特性。每个webapp使用自定义的WebappClassLoader(parent为shared classloader)来加载org.springframework.web.servlet.DispatcherServlet(继承了Servlet接口),这边的DispatcherServlet类相当于入口类,根据上面的隐式加载,会继续使用该classloader加载相关联的类。每个webappClassLoader是同级关系,不会存在相互访问的问题,从而达到不同webapp应用隔离的目的。

自定义classloader以及如何打破双亲委派机制

正是因为classloader有着上面的特性:双亲委派,隐式加载,隔离性,所以经常会有自定义classloader的需求。
自定义classloader之后,可以与原有classloader加载的类隔离开来,从而可以避免对原有classloader加载的类造成干扰。同时可以覆盖loadClass方法和findClass方法,打破双亲委派机制,实现自定义的class路径加载。下面举几个例子:

  1. OSGI不同bundle之间的隔离
    OSGI是Java动态化模块化系统,会有多个部署单元,每个部署单元称为一个bundle。每个bundle有自己独立的classloader,同时一个bundle又可以使用其他bundle导出的package,相当于委托另外一个bundle的classloader进行类的加载。
  2. 蚂蚁金服开源的sofa-ark框架
    sofa-ark是一款基于Java实现的轻量级类隔离加载容器,sofa-ark包含三个概念:
    sofa-ark模板图

    ark plugin和ark biz都是以jar包的形式存在,其中每个ark plugin使用自定义的PluginClassLoader来加载,每个ark biz也使用自定义的BizClassLoader来加载。这样可以使不同的ark plugin和不同的ark biz隔离开来,以PluginClassLoader为例:
public class PluginClassLoader extends AbstractClasspathClassloader {
   ...
    @Override
    protected Class<?> loadClassInternal(String name, boolean resolve) throws ArkLoaderException {

        // 1. sun reflect related class throw exception directly
        if (classloaderService.isSunReflectClass(name)) {
            throw new ArkLoaderException(
                String
                    .format(
                        "[ArkPlugin Loader] %s : can not load class: %s, this class can only be loaded by sun.reflect.DelegatingClassLoader",
                        pluginName, name));
        }

        // 2. findLoadedClass
        Class<?> clazz = findLoadedClass(name);

        // 3. JDK related class
        if (clazz == null) {
            clazz = resolveJDKClass(name);
        }

        // 4. Ark Spi class
        if (clazz == null) {
            clazz = resolveArkClass(name);
        }

        // 5. Import class export by other plugins
        if (clazz == null) {
            clazz = resolveExportClass(name);
        }

        // 6. Plugin classpath class
        if (clazz == null) {
            clazz = resolveLocalClass(name);
        }

        // 7. Java Agent ClassLoader for agent problem
        if (clazz == null) {
            clazz = resolveJavaAgentClass(name);
        }

        if (clazz != null) {
            if (resolve) {
                super.resolveClass(clazz);
            }
            return clazz;
        }

        throw new ArkLoaderException(String.format(
            "[ArkPlugin Loader] %s : can not load class: %s", pluginName, name));
    }
  ...
}

loadClass方法会调用loadClassInternal方法,当Plugin在运行时发现一个类需要被加载时,会按照如下步骤搜索:

  1. 如果已加载过,那就返回已加载好的那个类。
  2. 如果这个类是JDK自己的,那么就用JDKClassLoader去加载。
  3. 如果这个类是属于Ark容器的,那么就用ArkClassLoader去加载。
  4. 如果这个类是某个插件export的,那么就用ExportClassLoader去加载。
  5. 如果这个类是插件自身的,那么就用当前的ClassLoader直接loadClass就好。
  6. 最后使用某个java agent尝试加载。
  7. 实在找不到就报错。

可以看到该步骤并没有使用双亲委派的机制,而是自定义的加载策略。

context classloader作用

context classloader概念

经常会在代码里面看到这样的代码:

ClassLoader cl= Thread.currentThread().getContextClassLoader();
Class<?> clazz= cl.loadClass(getClassName());

每一个Thread都有一个相关联的context classloader,可以通过Thread.setContextClassLoader()方法设置。如果没有主动设置,Thread默认继承Parent Thread的 context classloader。如果你整个应用中都没有对此作任何处理,那么 所有的Thread都会以system classLoader作为context Classloader。

context classloader场景

可以自定义classloader,并设置到线程中,这样在当前线程的任何地方都可以使用该classloader进行显示加载,即调用loadClass或者forName方法。从而可以灵活的使用自定义的classloader,而不被java自带的classloader所限制。

最常见的是在Java的SPI场景中使用,比如JDBC。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进classpath里面。SPI的接口是Java核心库的一部分,是由bootstrap classloader来加载的,SPI的实现类一般由system classloader来加载。
以JDBC为例,直接看源码,省略了非关键代码:

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    ...
    private static void loadInitialDrivers() {
  
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ...
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
       ...
}

DriverManager类是JDK核心类,会被bootstrap classloader加载,在加载的时候会调用static代码块加载JDBC驱动,在loadInitialDrivers方法里面调用ServiceLoader.load(Driver.class),ServiceLoader是SPI的是一种实现,所谓SPI,即Service Provider Interface,用于一些服务提供给第三方实现或者扩展,可以增强框架的扩展或者替换一些组件。 继续看load方法。

public final class ServiceLoader<S>, implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

   private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }
}

可以看到会将context classloader传递给ServiceLoader,并最终赋值给loader属性,在调用driversIterator.next()遍历时,最终会调用nextService方法,可以看到nextService方法里面调用Class.forName(cn, false, loader)进行类的隐式加载。其中cn为所有通过spi方式注册的driver,比如mysql驱动的类名为com.mysql.jdbc.Driver,配置在mysql-connector-java-5.1.41.jar里的META-INF/services/java.sql.Driver文件中:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

通过这种方式保证了bootstrap classloader加载的DriverManager类可以访问由system classloader加载的具体SPI实现类com.mysql.jdbc.Driver。

参考文档

理解Java ClassLoader机制
springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载
浅议tomcat与classloader
通过tomcat源码查看其如何实现应用相互隔离
sofa-ark官方文档
真正理解ContextClassLoader

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272