概述
- 什么是类加载器
- 类加载器的分类
- 类加载器的层级(父子)关系
- 类加载器的特性
什么是类加载器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,下面可以通过几个源代码来佐证这一点。
- 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()方法中已经给出过程,下面我们稍微整理下这个流程来具体看看。
- 从jvm缓存中去find是否已经被加载过,如果是,那么直接返回加载后的Class对象
- 如果jvm从未加载过该类,那么让当前加载器的parent去加载
- 如果parent也为null,那么去找BootstrapClassLoader来加载
- 如果要加载的类并不在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例子
- spring boot是一个Java生态中非常火的框架
- 既然是框架,肯定有启动的过程
- 在启动的过程中肯定会触发一些事件,我们叫做ApplicationEvent
- 当这些事件被触发后,肯定有一个或者若干个监听器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;
}