深入分析理解java类加载器ClassLoader的加载机制

1、什么是类加载器?

在这里插入图片描述
可以看到上面这个简单流程就是我们运行java代码的整个过程,首先JVM将java源文件编译成.class字节码文件,然后用类加载器将class文件载入到内存供我们使用。可以看出ClassLoader在其中扮演着非常重要的作用。

2、java中有哪些类加载器?

首先我们需要知道JVM基础自带的默认三种类加载器,分别是启动类加载器Bootstrap ClassLoader扩展类加载器Extension ClassLoader应用程序类加载器Application ClassLoader。它们三者的关系如下:
在这里插入图片描述

我们先看一下这三个类加载器分别的加载路径是什么:

2.1 BootstrapClassLoader的加载路径

		System.out.println("BootstrapClassLoader 的加载路径: ");
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url.getPath());
        }
BootstrapClassLoader 的加载路径: 
/F:/installSoftware/JDK8/jre/lib/resources.jar
/F:/installSoftware/JDK8/jre/lib/rt.jar
/F:/installSoftware/JDK8/jre/lib/sunrsasign.jar
/F:/installSoftware/JDK8/jre/lib/jsse.jar
/F:/installSoftware/JDK8/jre/lib/jce.jar
/F:/installSoftware/JDK8/jre/lib/charsets.jar
/F:/installSoftware/JDK8/jre/lib/jfr.jar
/F:/installSoftware/JDK8/jre/classes

可以看出BootstrapClassLoader的加载路径就是JDK安装路径下:%JAVA_HOME%jre/lib

2.2 EXTClassLoader 的加载路径

		URLClassLoader extClassloader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
        System.out.println("EXTClassLoader 的加载路径:");
        URL[] urls = extClassloader.getURLs();
        for (URL url : urls) {
            System.out.println(url.getPath());
        }
EXTClassLoader 的加载路径:
/F:/installSoftware/JDK8/jre/lib/ext/access-bridge-64.jar
/F:/installSoftware/JDK8/jre/lib/ext/ClassLoaderTest.class
/F:/installSoftware/JDK8/jre/lib/ext/cldrdata.jar
/F:/installSoftware/JDK8/jre/lib/ext/dnsns.jar
/F:/installSoftware/JDK8/jre/lib/ext/jaccess.jar
/F:/installSoftware/JDK8/jre/lib/ext/jfxrt.jar
/F:/installSoftware/JDK8/jre/lib/ext/localedata.jar
/F:/installSoftware/JDK8/jre/lib/ext/nashorn.jar
/F:/installSoftware/JDK8/jre/lib/ext/sunec.jar
/F:/installSoftware/JDK8/jre/lib/ext/sunjce_provider.jar
/F:/installSoftware/JDK8/jre/lib/ext/sunmscapi.jar
/F:/installSoftware/JDK8/jre/lib/ext/sunpkcs11.jar
/F:/installSoftware/JDK8/jre/lib/ext/zipfs.jar

可以看出EXTClassLoader 的加载路径也是JDK安装路径下:%JAVA_HOME%jre/lib/ext

2.3 AppClassLoader 的加载路径

项目目录结构如下:
在这里插入图片描述

		URLClassLoader appClassloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        System.out.println("AppClassLoader 的加载路径:");
        urls = appClassloader.getURLs();
        for (URL url : urls) {
            System.out.println(url.getPath());
        }

在这里插入图片描述

可以看到AppClassLoader主要加载的是三个路径:

  • External Libraries依赖的外部jar包路径
  • 项目源代码被编译过后的项目路径out/production/algorithm(如果是maven项目,则该路径是target/classes/),同时在项目路径下被标记为resources的文件夹也会被加载。
  • idea安装目录下面的idea_rt.jar

3、类的加载过程

从上面我们已经知道了BootstrapClassLoader主要加载的是JDK路径下/lib/的java的一些核心类库,像rt.jar包含了java.lang.*、java.util.*等;EXTClassLoader 主要加载的是/lib/ext/下面的扩展类;AppClassLoader主要加载的是我们用户应用下面的类库。
因此我们先从最接近我们的类加载器AppClassLoader入手看一下具体的加载过程:
下图是AppClassLoader和EXTClassLoader 的类继承关系,很显然,AppClassLoader和EXTClassLoader 并不是我想象中的逻辑上的父子继承关系,那他们又是怎样的关系按照什么样的逻辑来处理?

在这里插入图片描述

3.1 寻找AppClassLoader的parent、ExtClassLoader的parent是什么?

首先我们得从java应用的入口类Launcher类开始入手。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private ClassLoader loader;
    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);
    }
 }

(上面代码有删减,只保留的关键部分)

  • 第2行初始化了一个Launcher 对象。
  • 第3行声明了一个ClassLoader 类型的变量loader
  • 第5行声明了一个ExtClassLoader类型的变量var1
  • 第7行将ExtClassLoader的一个实例(getExtClassLoader()获取的是一个单例)赋值给var1
  • 重点来了!!!第13行Launcher.AppClassLoader.getAppClassLoader()方法,传的参数是ExtClassLoader对象,好,我们现在进入getAppClassLoader这个方法(下面主要关注ExtClassLoader对象的走向)。
		public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
  • 第一行 System.getProperty(“java.class.path”);表示的就是我们上面打印的AppClassLoader加载的全部路径
  • 其余的不用管,重点关注最后一行代码return new Launcher.AppClassLoader(var1x, var0);这行代码new了一个AppClassLoader对象,传的参数是我们上面Launcher构造方法调用getAppClassLoader传的参数var0(var0是ExtClassLoader对象),AppClassLoader构造方法如下。
		AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }

可以看到,var2(var2是ExtClassLoader对象)又被传入到了父类的构造方法中,在上面的类继承关系图中我已经知道URLClassLoader是AppClassLoader的父类,所以我们现在进入AppClassLoader的父类URLClassLoader的构造方法中。

public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
    }

在这里,我们知道parent就是上面传入的ExtClassLoader对象,第一行又将parent传入到了父类的构造方法中,我们进入URLClassLoader的父类SecureClassLoader的构造方法中。

protected SecureClassLoader(ClassLoader parent) {
        super(parent);
    }

同样,我们再进入SecureClassLoader的父类ClassLoader的构造方法中。

	private final ClassLoader parent;
	//将parent参数传到了这个构造方法,然后又传到了下面的私有构造方法中
 	protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
    }
    

可以看到在java应用初始化启动(当new了一个Launcher对象)时ExtClassLoader类加载器对象从AppClassLoader构造方法中被一路送到了ClassLoader实例中,并将其赋值给parent变量。说了这么多,只需记住一点:
AppClassLoader的parent是ExtClassLoader

然后我们再用同样的方法看看ExtClassLoader的parent是什么?

  • 首先进入ExtClassLoader的构造方法中。
		public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

可以发现调用父类构造器并传入的ClassLoader参数为空。因此我们照样可以得出结论:
ExtClassLoader的parent为null

知道了上面这两条重要信息,下面再学习Java中的类加载过程就容易多了!!!


3.2 ClassLoader加载的具体过程

我们先从AppClassLoader类入手,在这个类中我们可以看到loadClass这个方法, 这个方法重载了ClassLoader类的loadClass方法,下面简单来看一下方法的执行过程。

public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }
            if (this.ucp.knownToNotExist(var1)) {
            //从缓存中查找是否已经加载过var1,如果已经加载过则直接返回该实例var5
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }
                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
            //没有加载过该类,将参数传递给父类方法并调用loadClass
                return super.loadClass(var1, var2);
            }
        }

主要看上面两处注释的地方,如果没有加载过该类则调用父类的loadClass方法。

在这里插入图片描述
现在我们到父类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;
        }
    }
  • 和AppClassLoader中的第一步一样, 也是先调用findLoadedClass判断内存中是否已经加载过该class文件。
  • 然后调用parent的loadClass方法进行加载,还记得我们上面煞费苦心寻找的parent是什么吗?没错,这里的parent就是ExtClassLoader对象,也就是调用的ExtClassLoader的loadClass方法,但是ExtClassLoader并没有重载loadClass方法,所以递归调用当前loadClass方法。
  • 继续调用findLoadedClass方法判断内存中是否已经加载过该class文件,然后判断parent是否null,经过上面的分析,这里的parent为null,所以调用findBootstrapClassOrNull方法去查找是否已经加载过,这里最终调用的是下面这个native方法
private native Class<?> findBootstrapClass(String name);

到这里我们也可以看出ExtClassLoader中的parent为什么为null了,因为启动类加载器BootstrapClassLoader是由C++编写的类加载器,我们不能在java代码中拿到它的引用。

  • 继续向下执行代码,如果上面没有查找到该Class类实例,c==null为true,则调用findClass方法,注意,现在查找的是BootstrapClassLoader加载的路径sun.mic.boot.class
  • 如果没找到,则返回null,返回到上一层递归的位置,也就是c = parent.loadClass(name, false);执行结束,继续向下执行,c==null为true,则调用findClass方法,现在查找的是ExtClassLoader加载的路径java.ext.dirs
  • 如果没找到,则返回null,返回到上一层递归的位置,也就是c = parent.loadClass(name, false);执行结束,继续向下执行,c==null为true,则调用findClass方法,现在查找的是AppClassLoader加载的路径java.class.path
  • 如果以上都没有找到则报异常ClassNotFoundException。

现在总结下类加载的整个过程:
(1) 从缓存中查找这个class文件是否已经加载过,如果没有,则委托给父类加载器。
(2)递归,重复第一步操作。
(3)如果ExtClassLoader也没有加载过则使用BootstrapClassLoader查找缓存中是否已经加载过该class文件。如果查找为空,则在sun.mic.boot.class路径下查找,查找成功就返回,否则调用子加载器查找。
(4)如果上面BootstrapClassLoader没有查找成功,则使用ExtClassLoader加载在java.ext.dirs路径下查找,查找成功就返回,否则调用子加载器查找。
(5)如果上面ExtClassLoader没有查找成功,则使用AppClassLoader加载在java.class.path下查找,查找成功就返回,否则调用子类查找。
(6)如果都没有查找到,则抛出异常。

上面这个流程专业术语也叫双亲委托的加载流程,委托是从下往上,查找是从上往下的这个过程。

4 打破双亲委托机制?

在学习Tomcat时看到了那么一堆Tomcat自己定义的类加载器,也以为是按照双亲委托机制进行类加载,网上查了下资料发现并不是这样,同时结合着平时Tomcat部署web项目的经验发现如果还是用传统的双亲委托机制进行类加载肯定行不通,下面来分析下具体的原因。
在这里插入图片描述

4.1 Tomcat的类加载器

Tomcat主要是有上面这5个类加载器,在Tomcat的初始化类加载器中发现了这么一段源码(我这里Tomcat的版本是8.5):

private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader = this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

注意下面if条件判断如果读取的配置为空,则返回parent加载器!!!。

private ClassLoader createClassLoader(String name, ClassLoader parent)
            throws Exception {

        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;
        value = replace(value);
        ......
}

上面代码展示了commonLoader、catalinaLoader、sharedLoader三个类加载器的创建。createClassLoader的第二个参数是当前类加载器的父加载器,从这里我们也可以看出commonLoader是catalinaLoader、sharedLoader的父加载器,而commonLoader的父加载器设置为null,默认为AppClassLoader,而它们三者加载的路径都在catalina.properties中已经设置,分别对应common.loader、server.loader、shared.loader的配置。

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=

catalinaLoader、sharedLoader的加载路径为空,所以结合我们上面说的如果读取的配置为空,则直接返回parent加载器,所以实际上我们要是不配置server.loader和shared.loader的路径则catalinaLoader、sharedLoader这两个类加载器就是commonLoader,他们三者的父加载器默认都是AppClassLoader!!!。所以默认情况下这三个类加载器的父子关系应该是下面这样!!!
在这里插入图片描述
在WebappClassLoader的start方法中也发现了它的加载路径,如下。

public void start() throws LifecycleException {

        state = LifecycleState.STARTING_PREP;

        WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
        for (WebResource classes : classesResources) {
            if (classes.isDirectory() && classes.canRead()) {
                localRepositories.add(classes.getURL());
            }
        }
        WebResource[] jars = resources.listResources("/WEB-INF/lib");
        for (WebResource jar : jars) {
            if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                localRepositories.add(jar.getURL());
                jarModificationTimes.put(
                        jar.getName(), Long.valueOf(jar.getLastModified()));
            }
        }

        state = LifecycleState.STARTED;
    }

很显然,WebappClassLoader主要是加载web应用下/WEB-INF/classes/WEB-INF/lib两个路径下的文件。

4.2 Tomcat的各个类加载器的加载范围

commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问。
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见。
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见。
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见。

4.2 Tomcat是怎么打破双亲委托机制?

根据我们经常使用Tomcat的经验:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

答案:
spring根本不会去管自己被放在哪里,它统统使用当前线程上下文加载器来加载类,而当前线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直牛逼!!!,大概设置ContextClassLoader过程如下,实际过程要比这个复杂。

Thread.currentThread().setContextClassLoader(WebAppClassLoader);
Thread.currentThread().getContextClassLoader();

参考了下面这些大佬的文章:
Tomcat 类加载器之为何违背双亲委派模型
真正理解线程上下文类加载器(多案例分析)
一看你就懂,超详细java中的ClassLoader详解
JAVA Launcher简析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qzxl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值