Java类加载器初探

概述

  1. 什么是类加载器
  2. 类加载器的分类
  3. 类加载器的层级(父子)关系
  4. 类加载器的特性

什么是类加载器ClassLoader

Java语言的一般的执行流程需要经过:

  • Java源码(.java) 到字节码文件(.class)
  • 字节码文件到JVM(Java虚拟机

这两个流程。Java虚拟机是一个应用系统,相当于一个中介接口,构建在源代码与操作系统之间,用于屏蔽不同操作系统之间的差异,做到write once,run anywhere.

ClassLoader的作用就是把字节码文件加载到jvm中运行时起作用的。

类加载器的分类

Java原生的ClassLoader

  • 应用类加载器(AppClassLoader)
  • 扩展类加载器(ExtClassLoader)
  • 启动类加载器(BootstrapClassLoader)

用户自定义的ClassLoader

  • 主要通过继承ClassLoader来实现,当然还需要重载某些方法。

现在这里有一个问题:这些不同的类加载器有什么关系?在我的项目中那么多的类,那个类是由哪个类加载器来加载的?

AppClassLoader
System.err.println("ClassLoader.getSystemClassLoader():"+ClassLoader.getSystemClassLoader());
String[] paths = System.getProperty("java.class.path").split(";");
for (String path : paths){
	System.out.println(path);
}

//输出如下
ClassLoader.getSystemClassLoader():sun.misc.Launcher$AppClassLoader@18b4aac2
C:\Program Files\Java\jdk1.8.0_201\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar
E:\JAVACODE\algorithms-implement-by-java\target\classes
C:\Users\xlrainy\.m2\repository\org\jetbrains\annotations\17.0.0\annotations-17.0.0.jar
C:\Users\xlrainy\.m2\repository\org\apache\commons\commons-vfs2\2.0\commons-vfs2-2.0.jar
C:\Users\xlrainy\.m2\repository\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar
C:\Users\xlrainy\.m2\repository\org\apache\maven\scm\maven-scm-api\1.4\maven-scm-api-1.4.jar
C:\Users\xlrainy\.m2\repository\org\codehaus\plexus\plexus-utils\1.5.6\plexus-utils-1.5.6.jar
C:\Users\xlrainy\.m2\repository\org\apache\maven\scm\maven-scm-provider-svnexe\1.4\maven-scm-provider-svnexe-1.4.jar
C:\Users\xlrainy\.m2\repository\org\apache\maven\scm\maven-scm-provider-svn-commons\1.4\maven-scm-provider-svn-commons-1.4.jar
C:\Users\xlrainy\.m2\repository\regexp\regexp\1.3\regexp-1.3.jar
C:\Users\xlrainy\.m2\repository\commons-io\commons-io\2.2\commons-io-2.2.jar
C:\Users\xlrainy\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar
C:\Users\xlrainy\.m2\repository\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar
C:\Users\xlrainy\.m2\repository\log4j\log4j\1.2.17\log4j-1.2.17.jar
C:\Users\xlrainy\.m2\repository\io\netty\netty-all\4.1.22.Final\netty-all-4.1.22.Final.jar
D:\Program Files\JetBrains\IntelliJ IDEA 2018.3.5\lib\idea_rt.jar
ExtClassLoader
String[] paths = System.getProperty("java.ext.dirs").split(";");
for (String path : paths){
	System.out.println(path);
}

//输出
C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext
BootstrapClassLoader
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
	System.out.println(urls[i].toExternalForm());
}

//输出
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/resources.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/rt.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/jsse.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/jce.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/charsets.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/lib/jfr.jar
file:/C:/Program Files/Java/jdk1.8.0_201/jre/classes

又出现一个问题:貌似BootstrapClassLoader和ExtClassLoader加载的类都可以被AppClassLoader加载?

类加载器的层级(父子)关系以及一些特性

在这里插入图片描述
这种父子关系跟我们平时说的类之间的继承并不一样,也就是AppClassLoader并没有继承ExtClassLoader。而是通过组合的方式。具体在代码里证明了这一点。

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        //var1 是一个ExtClassLoader的引用
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        try {
        //ExtClassLoader的引用var1被当做参数传入了AppClassLoader的构造器。
            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);
        }
    }

这种父子关系具有的特性:可见性
可以保证,ExtClassLoader加载的类可以被AppClassLoader加载。即:父类加载的类对子类完全可见。

那ExtClassLoader的父亲是BootstrapClassLoader吗

答案是否定的。也就是说在这种层级的关系结构下,AppClassLoader的父亲确实是ExtClassLoader(也就是内部聚合了有个ExtClassLoader的实例),但是ExtClassLoader的父亲却不是BootstrapClassLoader,而是null,下面可以通过几个源代码来佐证这一点。

  1. ExtClassLoader的构造器
public ExtClassLoader(File[] var1) throws IOException {
	super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

//ExtClassLoader继承自URLClassLoader
public URLClassLoader(URL[] urls, ClassLoader parent,URLStreamHandlerFactory factory) 
  • ClassLoader的loadClass()方法(通常自定义的类加载器可以重写该方法,但是更常见的是重写其call的另外一个方法:findClass())
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
        // 会在jvm缓存中去找是否加载过这个类,如果加载过,那么直接返回,否则才会执行加载过程
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果当前的类加载器的parent为null,那么就会去找BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);	
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    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()的代码中,我们还可以得到一个关于类加载的特点:单一性。也就是说一个类只会被加载一次。那么,问题又来了:两个类满足什么条件,才算是同一个类?

  • 类名一样?
  • 全限定类名一样?(package name + class name)
  • or other?

至此,我们已经知道了类加载器的一些基本概念几种不同的类加载器,它们之间的层级关系(父子),以及每种类加载器负责加载什么样的类,以及可见性、单一性这两个特点。

那么,一个类究竟是如何被类加载器加载的呢?其实,在上面的loadClass()方法中已经给出过程,下面我们稍微整理下这个流程来具体看看。

  1. 从jvm缓存中去find是否已经被加载过,如果是,那么直接返回加载后的Class对象
  2. 如果jvm从未加载过该类,那么让当前加载器的parent去加载
  3. 如果parent也为null,那么去找BootstrapClassLoader来加载
  4. 如果要加载的类并不在BootstrapClassLoader负责的范围之内,那么会call一下findClass()这个方法来。

这种机制,就是我们常说的双亲委派机制。也就是在加载某个类的时候,会尽可能让其双亲parent来执行加载过程。

这样做的目的,这里就不再提了。

双亲委派机制就真的是无敌的?

通过上面的介绍,我们知道了类加载的过程是通过双亲委派机制来实现的,目的当然是出于安全、和避免重复加载的考虑。但是是不是这种方式就一定适用于所有场景?答案肯定不是的

我们都知道Java是一种充满设计感的语言,各种接口解耦的实现,最有代表性的基于接口的编程+策略模式+配置文件的服务提供接口模式(Service Provider Interface)。数据库驱动服务就是其中最有代表性的服务,当然还有其他的,比如事件监听器的实现等。

数据库厂商有很多,每个厂商可能都要提供一个驱动的jar包,比如MySQL中的com.mysql.jdbc.Driver,这个驱动其实是Java核心api的一个实现类,在Java的核心api中定义了一个数据库驱动应该具有什么样的行为方法,比如连接,执行,提交等等,所有的厂商都需要按照这个规范来实现自己的驱动,在代码中使用接口引用,避免对具体实现类进行硬编码,这样就可以做到系统模块之间的解耦。这是基于接口编程的好处。

当然,现在有一个MySQL驱动的jar包了,那肯定还需要配置文件来配置MySQL吧,常见的配置大家应该都知道。

接下来,对于系统来说,我得知道怎么找到这个服务的实现类?当然,Java中这是通过ServiceLoader来实现的,具体不在赘述。

好了,那么现在有一个问题,在类加载中有一个这样的特点:

如果一个类是由加载器A来加载的,那么这个类所依赖的其他类也要由A来加载。

那么问题来了,在Java 的核心api中定义的服务接口应该是由BootstrapClassLoader来加载,那么这些接口的实现类也应该由BootstrapClassLoader来进行加载?但是,我们都知道由用户自己提供的jar包肯定是由AppClassLoader来加载的啊,那么这自相矛盾了啊?

那么,系统底层具体是怎么解决这个问题的?

public final class ServiceLoader<S> implements Iterable<S> {
	//getContextClassLoader
    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 = svc;
        loader = cl;
        reload();
    }
ContextClassLoader

这个类加载器,是一个线程安全的类加载器,每个线程独有的,保存的是当前执行任务的线程上下文的类加载器,如果是在用户自定义的代码中,那么ContextClassLoader就是AppClassLoader。

ServiceLoader通过获取系统的线程上线文类加载器,然后使用这个类加载器去加载那些服务的实现类。最终你会发现,那些依赖于Java核心api的服务实现类最后却是由APPClassLoader来加载的,而不是加载核心api的BootstrapClassLoader。这就违反了双亲委派机制。

Spring Boot中的ContextClassLoader例子

  1. spring boot是一个Java生态中非常火的框架
  2. 既然是框架,肯定有启动的过程
  3. 在启动的过程中肯定会触发一些事件,我们叫做ApplicationEvent
  4. 当这些事件被触发后,肯定有一个或者若干个监听器ApplicationListener要去监听这些事件,并且做出相应的动作,比如记录日子,准备环境,etc。

那好,接下来,可以看看在spring boot中是怎么进行事件监听器这个服务的加载的。

private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
	}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
			Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
		Set<String> names = new LinkedHashSet<>(
				SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
				classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值