本文主要包含下面几个内容:
- classloader双亲委派机制以及classloader加载class的流程
- classloader的其他特性
- 自定义classloader以及如何打破双亲委派机制
- context classloader作用
classloader双亲委派机制以及classloader加载class的流程
java类加载流程
JVM启动时,有三个classloader负责加载class,如下:
- bootstrap classloader
- extension classloader
- system classloader
- bootstrap classloader:采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类; 这些类位于$JAVA_HOME/jre/lib/下面。当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
- extension classloader:扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar。
- 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;
}
}
//...
}
可以看到主要分为几步:
- 根据类名尝试从本地缓存里面获取已经加载的class,如果没有转2,如果有转最后一步。
- 判断parent是否为null:不为null,直接使用parent的classloader加载;为null,相当于parent是bootstrap classloader,使用bootstrap classloader加载。如果没有转3,如果有转最后一步。
- 调用
findClass
方法,根据一定的路径策略获取class,没有找到的话返回null,找到转最后一步。 - 解析class。
上面的几个步骤可以看到,优先由parent的classloader加载,这就是双亲委派机制。其中第3步可以覆盖 findClass方法,实现自己的加载策略:比如可以从远程网络class文件,从本地压缩包里面获取class文件等。
classloader的其他特性
除了双亲委派特性,classloader还有隐式加载,隔离等特性。
隐式加载
JVM加载class文件到内存有两种方式。
- 隐式加载:所谓隐式加载就是不通过在代码里调用classloader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中继承或者引用某个类时,JVM在解析当前这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
- 显式加载:相反的显式加载就是我们在代码中通过调用classloader类来加载一个类的方式,调用
this.getClass.getClassLoader().loadClass()
或者Class.forName()
,或者我们自己实现的ClassLoader的findClass()
方法等。
其实这两种方式是混合使用的,例如,我们通过自定义的classloader显式加载一个类时,这个类中又引用了其他类,那么这些类就是隐式加载的。正如所有程序都有一个main函数一样,所有的应用都有一个或多个入口的类,这个类是被最先加载的,并且随后的所有类都像树枝一样以此类为根被加载。
举两个例子:
- java程序运行的时候,都会首先从拥有main方法的入口类运行,该类由AppClassLoader加载,从而其他被它应用的类都会由AppClassLoader来加载。
- 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
进行加载。
隔离性
为了理解隔离性,需要先理解下面几个概念
- 不同的classloader加载的同一个class文件,会被jvm认为是不同的class。如果把一个ClassLoader创建的实例,赋值给另一个ClassLoader加载的类,会导致ClassCastException异常。
- class冲突,同一个classloader只能加载一个class name(包括package)的class,如果存在多个class name相同的类,会出现随机加载class,从而导致NoSuchMethodError等异常。
- 两个平级的classloader加载的两个类,不能相互访问,比如在下面的场景:
+-BootstrapClassLoader [bootstrap classloader]
+-sun.misc.Launcher$ExtClassLoader@7bf2dede [extension classloader]
+-sun.misc.Launcher$AppClassLoader@18b4aac2 [system classloader]
+-自定义的classloader1
+-自定义的classloader2
其中classloader1加载的class不能访问classloader2加载的class。
- 一个classloader可以访问父classloader加载的class,比如自定义的classloader1可以访问AppClassLoader加载的类。这是双亲委派机制决定的。
- 父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路径加载。下面举几个例子:
- OSGI不同bundle之间的隔离
OSGI是Java动态化模块化系统,会有多个部署单元,每个部署单元称为一个bundle。每个bundle有自己独立的classloader,同时一个bundle又可以使用其他bundle导出的package,相当于委托另外一个bundle的classloader进行类的加载。 - 蚂蚁金服开源的sofa-ark框架
sofa-ark是一款基于Java实现的轻量级类隔离加载容器,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在运行时发现一个类需要被加载时,会按照如下步骤搜索:
- 如果已加载过,那就返回已加载好的那个类。
- 如果这个类是JDK自己的,那么就用
JDKClassLoader
去加载。 - 如果这个类是属于Ark容器的,那么就用
ArkClassLoader
去加载。 - 如果这个类是某个插件export的,那么就用
ExportClassLoader
去加载。 - 如果这个类是插件自身的,那么就用当前的ClassLoader直接loadClass就好。
- 最后使用某个java agent尝试加载。
- 实在找不到就报错。
可以看到该步骤并没有使用双亲委派的机制,而是自定义的加载策略。
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