Java类加载器
类加载器负责把描述类的数据从class字节码文件加载到JVM,并对数据进行检验、解析和初始化,最终形成可以被JVM直接使用的Java类型。
一般在开发web项目时很少直接使用类加载器,因为web容器为我们屏蔽了类加载器的复杂性,而我们只需要实现具体的业务逻辑即可。而如果开发过Java中间件,你会发现类加载器使用非常频繁。
本章先介绍类加载器的API及使用,然后依次介绍ClassLoader的源码、JDK和web中间件的类加载器,最后介绍热加载技术的实现原理。
4.1 ClassLoader API
ClassLoader是一个抽象类,不能直接使用,因此我们需要继承并重写其中的方法。它的主要方法有defineClass、loadClass、findClass和resolveClass等及其重载方法。主要方法的定义如下:
// 方法的输入是字节码的byte数组,输出是Class对象,它的作用是将字节码数组解析为Class对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
// 通过类名称查找Class对象
public Class<?> loadClass(String name)
// 通过类名称查找类
protected Class<?> findClass(String name)
// 类加载后调用该方法完成类的链接
protected final void resolveClass(Class<?> c)
先来实现一个简单的类加载器NetworkClassLoader,这个类加载器具备从网络加载类文件的能力, 实现代码如下。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class NetworkClassLoader extends ClassLoader {
// 下载地址
private String downloadUrl;
public NetworkClassLoader(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
// 实现类的查找方法
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// 从远程下载类文件,从而获得类的字节码数组
private byte[] loadClassData(String name) {
// load the class data from the connection
// ...
}
// 类名称转化为服务器下载的地址
private String classNameToPath(String name) {
return downloadUrl + "/" + name.replace(".", "/") + ".class";
}
// 测试方法
public class Main {
public static void main(String[] args) throws Exception {
// 下载地址
String baseUrl = "https://wwww.jrasp.com";
// 初始化网络类加载器
NetworkClassLoader loader = new NetworkClassLoader(baseUrl);
// 加载位于 https://wwww.jrasp.com/Foo.class的类,并创建实例
Object foo = loader.loadClass("Foo").newInstance();
}
}
}
被加载的类Foo是一个简单类,在创建实例对象时输出"create new instance",Foo类的代码如下。
public class Foo {
public Foo() {
System.out.println("create new instance");
}
}
// 运行Main方法,输出结果如下:
// create new instance
ClassLoader主要功能是类查找、加载和链接,除了加载类之外,类加载器还负责加载资源如配置文件或图片等。
4.2 ClassLoader源码解析
有了上面的使用基础,再来分析下类加载器及其重要实现类的源码。
4.2.1 loadClass
ClassLoader调用其loadClass方法来加载class,loadClass核心代码如下:
代码位置:src/java.base/share/classes/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先, 检查类是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 当前类加载器的父加载不为空,尝试从父类加载器加载
c = parent.loadClass(name, false);
} else {
// 父加载器为空,使用启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 忽略异常,继续查找
}
if (c == null) {
// 父加载器加载不到,调用当前类加载器重写的findClass查找
c = findClass(name);
}
}
// 链接类
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面的类加载顺序可以总结为:优先尝试父加载器去加载(如果父加载器为null,则调用系统类加载器BootstrapClassLoader去加载),父加载器都尝试失败后才会交由当前ClassLoader重写的findClass方法去查找。如下图4-1所示:
图4-1 类加载器的委托模型
4.2.2 findClass
在加载class的过程中,如果父加载器都没有找到,则调用子类加载器重写的findClass方法继续查找,findClass方法如下:
代码位置:src/java.base/share/classes/java/lang/ClassLoader.java
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 调用时抛出异常
throw new ClassNotFoundException(name);
}
可以看到该方法里面抛出异常,因此不能直接调用,需要子类来实现。URLClassLoader是ClassLoader的子类并重写了findClass方法。URLClassLoader的属性与构造器如下:
代码位置:src/java.base/share/classes/java/net/URLClassLoader.java
// 类和资源的查找路径
private final URLClassPath ucp;
public URLClassLoader(URL[] urls, ClassLoader parent) {
// 指定父加载器
super(parent);
// ... 权限检查代码省略
this.acc = AccessController.getContext();
// 初始化 ucp 属性
ucp = new URLClassPath(urls, acc);
}
实现ClassLoader的findClass方法加载指定路径下的类。
代码位置:src/java.base/share/classes/java/net/URLClassLoader.java
protected Class<?> findClass(final String name) throws ClassNotFoundException {
// 1、将类的全限定名变成.class文件路径的方式
String path = name.replace('.', '/').concat(".class");
// 2、在URLClassPath中查找是否存在
Resource res = ucp.getResource(path, false);
// ... 异常处理忽略
return defineClass(name, res);
}
URLClassLoader的findClass方法的执行逻辑主要分为三步:
- 将类的全限定名变成.class文件路径的方式;
- 在URL中查找文件是否存在;
- 调用defineClass完成类的链接和初始化;
4.2.3 defineClass
defineClass与findClass一起使用,findClass负责读取来自磁盘或网络的字节码,而defineClass将字节码解析为Class对象, 在defineClass方法中使用resolveClass方法完成对Class的链接。源代码如下:
代码位置:src/java.base/share/classes/java/lang/ClassLoader.java
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
// 调用native方法完成链接
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
defineClass的实现在defineClass1方法中,defineClass1是一个native方法,具体实现hotspot中,实现较为复杂,一般不需要特别关注。 ClassLoader加载一个class文件到JVM时需要经过的步骤,如下图4-2所示:
图4-2 JVM加载类的阶段
一般我们只需要重写ClassLoader的findClass方法获取需要加载的类的字节码,然后调用defineClass方法生成Class对象。如果想要在类加载到JVM中时就被链接,可以调用resolveClass方法,也可以选择交给JVM在类初始化时链接。
4.3 JDK的类加载器
JDK自身的jar包如rt.jar和tools.jar(或者JDK9以上的模块)等中的类也需要使用类加载器来加载,下面的代码用来获取JDK内置的类加载器。
public class JdkClassloader {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取系统类加载器的父类加载器 --> 扩展类加载器或者平台类加载器
ClassLoader platformClassLoader = systemClassLoader.getParent();
System.out.println(platformClassLoader);
// 获取扩展类加载器的父类加载器 --> 启动类加载器(C/C++)
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println(bootstrapClassLoader);
}
}
在JDK8上运行:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4a574795
null
在JDK11上运行:
jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17
jdk.internal.loader.ClassLoaders$PlatformClassLoader@3cda1055
null
可以看到JDK8和JDK11类加载器的类名称存在差异,下面分别说明其实现。
4.3.1 JDK8的类加载器
4.3.1.1 AppClassloader
AppClassloader也称为System ClassLoader,继承了URLClassLoader,是Java虚拟机默认的类加载器之一,主要用来加载用户类和第三方依赖包,在JVM启动命令行中设置-Djava.class.path
参数来指定加载路径。
代码位置:src/share/classes/sun/misc/Launcher$AppClassLoader.java
// AppClassLoader继承URLClassLoader
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException {
// 搜索路径java.class.path
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}
// 重写了loadClass
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 调用父类URLClassLoader完成类加载
return (super.loadClass(name, resolve));
}
// 其他方法省略...
}
4.3.1.2 ExtClassLoader
ExtClassLoader称为扩展类加载器,继承了URLClassLoader,主要负责加载Java的扩展类库,默认加载${JAVA_HOME}/jre/lib/ext/目录下的所有jar包,也可以用参数-Djava.ext.dirs
来设置它的搜索路径。
代码位置:src/share/classes/sun/misc/Launcher$ExtClassLoader.java
// ExtClassLoader继承URLClassLoader
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException {
final File[] dirs = getExtDirs();
try {
return new ExtClassLoader(dirs);
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
private static File[] getExtDirs() {
// 通过系统变量指定加载路径
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
}
JDK8的类加载器的继承关系如下图4-3所示:
图4-3 JDK8的类加载器的继承关系
4.3.1.3 JDK8的类加载器的初始化
JDK的类加载器的初始化在Launcher类中。
源码位置:src/share/classes/sun/misc/Launcher.java
public class Launcher {
public Launcher() {
// 创建ExtClassLoader
ClassLoader extcl = ExtClassLoader.getExtClassLoader();
// 创建AppClassLoader
ClassLoader loader = AppClassLoader.getAppClassLoader(extcl);
// 设置当前线程的ContextClassLoader
Thread.currentThread().setContextClassLoader(loader);
// 异常处理的代码省略
}
// ...
}
可以看到,初始化过程较为简单,先初始化ExtClassLoader,然后在初始化AppClassLoader,并且设置AppClassLoader的父加载器为ExtClassLoader。
4.3.2 JDK11的类加载器
JDK9实现模块化之后,对Classloader有所改造,其中一点就是将ExtClassLoader改为PlatformClassLoader, 模块化之后不同的Classloader加载各自对应的模块。因为JDK11是一个长期支持的稳定版本,这里以JDK11的源代码来说明类加载器的变化。JDK11的类加载器的继承关系如下图4-4所示:
图4-4 JDK11的类加载器的继承关系
4.3.2.1 BuiltinClassLoader
BuiltinClassLoader是PlatformClassLoader、BootClassLoader和AppClassloader的父类,功能上与URLClassLoader相似,都是基于UrlClassPath来实现类的查找,但BuiltinClassLoader还支持从模块中加载类。
BuiltinClassLoader的属性与构造函数如下:
代码位置:src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
// 类加载器路径
private final URLClassPath ucp;
BuiltinClassLoader(String name, BuiltinClassLoader parent, URLClassPath ucp) {
// 确保当父加载器是bootloader时返回null
// name 是类加载器的名称
super(name, parent == null || parent == ClassLoaders.bootLoader() ? null : parent);
this.parent = parent;
this.ucp = ucp;
this.nameToModule = new ConcurrentHashMap<>();
this.moduleToReader = new ConcurrentHashMap<>();
}
BuiltinClassLoader也重写了loadClass方法,loadClass实际调用loadClassOrNull方法,来看下loadClassOrNull方法的实现。
源码位置:src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
protected Class<?> loadClassOrNull(String cn, boolean resolve) {
// 加锁,保证线程安全
synchronized (getClassLoadingLock(cn)) {
// 先去找一次class是否已经被加载了,此方法是ClassLoader中的native方法
Class<?> c = findLoadedClass(cn);
if (c == null) {
// 这里会需要去先加载模块信息
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
BuiltinClassLoader loader = loadedModule.loader();
if (loader == this) {
if (VM.isModuleSystemInited()) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// 委托其他类加载器加载
c = loader.loadClassOrNull(cn);
}
} else {
// 先调用父加载器的相关方法去加载一次
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
// 如果没加载到,则用当前加载器去加载
if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
// 此方法内会调用到defineClas方法完成类的定义
c = findClassOnClassPathOrNull(cn);
}
}
}
if (resolve && c != null)
resolveClass(c);
return c;
}
}
和通常的双亲委派稍有差异,如果一个class属于某个module那么会直接调用该module的类加载器去加载, 而不是说直接用当前类加载器的双亲委派模型去加载。 但是找到这个class对应的类加载器后,还是会按照双亲委派去加载。
BuiltinClassLoader也重写了ClassLoader的findClass方法。
源码位置:src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
@Override
protected Class<?> findClass(String cn) throws ClassNotFoundException {
// 在模块中尝试查找
LoadedModule loadedModule = findLoadedModule(cn);
Class<?> c = null;
if (loadedModule != null) {
// 加载任务委派给模块的加载器
if (loadedModule.loader() == this) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// 类路径下查找
if (hasClassPath()) {
c = findClassOnClassPathOrNull(cn);
}
}
// 都没有找到,抛出异常
if (c == null)
throw new ClassNotFoundException(cn);
return c;
}
其中findClassOnClassPathOrNull是在类路径下查找类。
源码位置:src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
private Class<?> findClassOnClassPathOrNull(String cn) {
String path = cn.replace('.', '/').concat(".class");
// 权限检查代码省去...
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(cn, res);
} catch (IOException ioe) {
// TBD on how I/O errors should be propagated
}
}
return null;
}
4.3.2.2 BuiltinClassLoader的子类以及初始化
ClassLoaders类中分别初始化BootClassLoader、PlatformClassLoader和AppClassLoader类加载器。
源码位置:src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java
public class ClassLoaders {
// JDK内置类加载器
private static final BootClassLoader BOOT_LOADER;
private static final PlatformClassLoader PLATFORM_LOADER;
private static final AppClassLoader APP_LOADER;
// 初始化类加载器对象
static {
// 可以使用 -Xbootclasspath/a 或者 -javaagent 中的Boot-Class-Path属性指定
String append = VM.getSavedProperty("jdk.boot.class.path.append");
// 初始化BOOT_LOADER
BOOT_LOADER =
new BootClassLoader((append != null && append.length() > 0)
? new URLClassPath(append, true)
: null);
// 初始化PLATFORM_LOADER并指定AppClassLoader的父加载器BOOT_LOADER
PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
// 获取classpath路径
String cp = System.getProperty("java.class.path");
if (cp == null || cp.length() == 0) {
String initialModuleName = System.getProperty("jdk.module.main");
cp = (initialModuleName == null) ? "" : null;
}
URLClassPath ucp = new URLClassPath(cp, false);
// 初始化APP_LOADER并指定AppClassLoader的父加载器为PLATFORM_LOADER
APP_LOADER = new AppClassLoader(PLATFORM_LOADER, ucp);
}
// ...
}
从类加载器实例的初始化代码可以看出,BootClassLoader用来加载jdk.boot.class.path.append
参数指定的类,在初始化PLATFORM_LOADER是指定BOOT_LOADER为其父类,在初始化AppClassLoader是指定PLATFORM_LOADER为其父类,构成了类加载器的三层结构。
再来看下JDK9以上特有的PlatformClassLoader类:
private static class PlatformClassLoader extends BuiltinClassLoader {
PlatformClassLoader(BootClassLoader parent) {
// 类加载器名称为platform
super("platform", parent, null);
}
// ...
}
不同类加载器负责加载对应的模块,在编译JDK时指定。
代码来源:jdk11-1ddf9a99e4ad/make/common/Modules.gmk
- BOOT_MODULES是由引导加载程序定义的模块:
java.base java.datatransfer
java.desktop java.instrument
java.logging java.management
java.management.rmi java.naming
java.prefs java.rmi
java.security.sasl java.xml
jdk.internal.vm.ci jdk.jfr
jdk.management jdk.management.jfr
jdk.management.agent jdk.net
jdk.sctp jdk.unsupported
jdk.naming.rmi
- PLATFORM_MODULES是由平台加载程序定义的模块:
java.net.http java.scripting
java.security.jgss java.smartcardio
java.sql java.sql.rowset
java.transaction.xa java.xml.crypto
jdk.accessibility jdk.charsets
jdk.crypto.cryptoki jdk.crypto.ec
jdk.dynalink jdk.httpserver
jdk.jsobject jdk.localedata
jdk.naming.dns jdk.scripting.nashorn
jdk.security.auth jdk.security.jgss
jdk.xml.dom jdk.zipfs
jdk.crypto.mscapi jdk.crypto.ucrypto
java.compiler jdk.aot
jdk.internal.vm.compiler
jdk.internal.vm.compiler.management
java.se
- JRE_TOOL_MODULES是JRE中包含的工具,由AppClassLoader加载:
jdk.jdwp.agent
jdk.pack
jdk.scripting.nashorn.shell
未列出的其他模块由AppClassLoader加载。
4.4 Web容器的加载器
前面介绍了Java中类加载的一般模型:双亲委派模型,这个模型适用于大多数类加载的场景,但对于web容器却是不适用的。这是因为servlet规范对web容器的类加载做了一些规定,简单的来说有以下几条:
- WEB-INF/classes和WEB-INF/lib路径下的类会优先于父容器中的类加载。例如WEB-INF/classes下有个Foo类,CLASSPATH下也有个Foo类,web容器加载器会优先加载位于WEB-INF/classes下的类,这与双亲委托模型的加载行为相反。
- java.lang.Object等系统类不遵循第一条。WEB-INF/classes或WEB-INF/lib中的类不能替换系统类。对于哪些是系统类,其实没有做出具体规定,web容器通常是通过枚举了一些类来进行判断的。
- web容器的自身的实现类不被应用中的类引用,即web容器的实现类不能被任何应用类加载器加载。对于哪些是web容器的类也是通过枚举包名称来进行判断。
4.4.1 Jetty类加载器
为了实现上面的三个要求并实现不同部署应用间依赖的隔离,Jetty定义了自己的类加载器WebAppClassLoader,类加载器的继承关系如下:
图4-5 Jetty类加载器的继承关系
WebAppClassLoader的属性如下:
// 类加载器上下文
private final Context _context;
// 父加载器
private final ClassLoader _parent;
// 加载文件的后缀 .zip或者.jar
private final Set<String> _extensions = new HashSet<String>();
// 加载器名称
private String _name = String.valueOf(hashCode());
// 类加载之前转换器
private final List<ClassFileTransformer> _transformers = new CopyOnWriteArrayList<>();
当类的package路径名位包含于以下路径时,会被认为是系统类。系统类是对应用类可见。
// 系统类不能被应用jar包中的类替换,并且只能被system classloader加载
public static final ClassMatcher __dftSystemClasses = new ClassMatcher(
"java.","javax.","org.xml.","org.w3c."
);
Server类不对任何应用可见,Jetty同样是用package路径名来区分哪些是Server类。WebAppContext中配置如下:
// 使用system classloader加载,并且对web application不可见
public static final ClassMatcher __dftServerClasses = new ClassMatcher(
"org.eclipse.jetty."
);
我们可以通过WebAppContext.addServerClasses或 WebAppContext.addServerClassMatcher方法设置 Server 类。 需要注意的是,Server 类是对所有应用都不可见,但是 WEB-INF/lib 下的应用类可以替换 Server 类。
代码位置:jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
public static void addServerClasses(Server server, String... pattern) {
addClasses(__dftServerClasses, SERVER_SRV_CLASSES, server, pattern);
}
public static void addSystemClasses(Server server, String... pattern) {
addClasses(__dftSystemClasses, SERVER_SYS_CLASSES, server, pattern);
}
public void addServerClassMatcher(ClassMatcher serverClasses) {
_serverClasses.add(serverClasses.getPatterns());
}
public void addSystemClassMatcher(ClassMatcher systemClasses) {
_systemClasses.add(systemClasses.getPatterns());
}
WebAppClassLoader的构造函数如下:
public WebAppClassLoader(ClassLoader parent, Context context)
throws IOException {
// 指定父加载器
super(new URL[]{}, parent != null ? parent
: (Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader()
: (WebAppClassLoader.class.getClassLoader() != null ? WebAppClassLoader.class.getClassLoader()
: ClassLoader.getSystemClassLoader())));
_parent = getParent();
_context = context;
if (_parent == null)
throw new IllegalArgumentException("no parent classloader!");
// 类加载器可以加载的文件类型:jar或者zip包
_extensions.add(".jar");
_extensions.add(".zip");
}
构造函数可以显示指定父类加载器,默认情况下为空,即将当前的线程上下文classLoader指定为当前的parent, 而这个线程上下文classLoader如果没有用户指定的话默认又将是前面提到过的System ClassLoader。
再看下loadClass方法。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
ClassNotFoundException ex = null;
Class<?> parentClass = null; // 来源于父加载器
Class<?> webappClass = null; // 来源于webapp加载器
// 先从已经加载的类中查找
webappClass = findLoadedClass(name);
if (webappClass != null) {
return webappClass;
}
// 先尝试从当前类加载器加载(这里true表示检查类是否是系统类,如果不是,返回加载的类)
webappClass = loadAsResource(name, true);
if (webappClass != null) {
return webappClass;
}
// 然后尝试当前类加载器的父加载器加载
try {
parentClass = _parent.loadClass(name);
// 判断是否允许加载server类,或者当前类不是 server 类
if (Boolean.TRUE.equals(__loadServerClasses.get())
|| !_context.isServerClass(parentClass)) {
return parentClass;
}
} catch (ClassNotFoundException e) {
ex = e;
}
// 尝试从当前类加载器加载(这里false表示不检查类是否是系统类)
webappClass = loadAsResource(name, false);
if (webappClass != null) {
return webappClass;
}
throw ex == null ? new ClassNotFoundException(name) : ex;
}
}
4.4.2 Tomcat类加载器
与Jetty容器一样,Tomcat也需要遵循servlet三条规范。Tomcat的类加载器的继承关系如下图4-6所示。
图4-6 Tomcat类加载器的继承关系
4.4.2.1 WebappClassLoader
代码来源:apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappLoader.java
public class WebappClassLoader extends WebappClassLoaderBase {
public WebappClassLoader() {
super();
}
public WebappClassLoader(ClassLoader parent) {
super(parent);
}
//...
}
WebappClassLoader继承WebappClassLoaderBase,类加载的功能主要在WebappClassLoaderBase中实现。直接看WebappClassLoaderBase的代码,WebappClassLoaderBase是一个抽象类,继承了URLClassLoader并重写了loadClass方法。
代码来源:apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java
先来看下其属性与构造函数:
// 是否使用双亲委托模型
protected boolean delegate = false;
// 加载JavaSE的类加载器
private ClassLoader javaseClassLoader;
// 当前类加载器的父加载器
protected final ClassLoader parent;
protected WebappClassLoaderBase() {
super(new URL[0]);
// 初始化没有指定父加载器,则父加载器为系统类加载器
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
// 初始化javaseClassLoader为平台类加载器或者扩展类加载器
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.javaseClassLoader = j;
}
对loadClass进行了重写:
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass0(name);
if (clazz != null) return clazz;
// Web应用程序本地类缓存中没有,可以从系统类加载器缓存中查找,
// 如果找到说明AppClassLoader之前已经加载过这个类
clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// 将类似java.lang.String这样的类名这样转换成java/lang/String
String resourceName = binaryNameToPath(name, false);
// 获取引导类加载器(BootstrapClassLoader)
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// 引导类加载器根据转换后的类名获取资源url,如果url不为空,就说明找到要加载的类
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// ...
}
// 首先,从扩展类加载器(ExtClassLoader)加载
if (tryLoadingFromJavaseLoader) {
return javaseLoader.loadClass(name);
}
// delegate允许类委托给父类加载
boolean delegateLoad = delegate || filter(name, true);
if (delegateLoad) {
return Class.forName(name, false, parent);
}
// 在当前web路径加载
clazz = findClass(name);
// 经过上面几个步骤还未加载到类,则采用系统类加载器(也称应用程序类加载器)进行加载
if (!delegateLoad) {
return Class.forName(name, false, parent);
}
}
// 最终,还未加载到类,抛出类未找到的异常
throw new ClassNotFoundException(name);
}
4.2.2.3 JSP类加载器
JSP类加载器同样也是继承于URLClassLoader并重写了loadClass,来看下他的源码。
代码来源:apache-tomcat-10.1.13-src/java/org/apache/jasper/servlet/JasperLoader.java
public class JasperLoader extends URLClassLoader {
@Override
public synchronized Class<?> loadClass(final String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
// 从JVM的类缓存中查找
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// SecurityManager 代码省略
// 如果类名不是以org.apache.jsp包名开头的,则使用WebappClassLoader加载
if( !name.startsWith(Constants.JSP_PACKAGE_NAME + '.') ) {
clazz = getParent().loadClass(name);
if( resolve ) {
resolveClass(clazz);
}
return clazz;
}
// 如果是org.apache.jsp包名开头JSP类,就调用父类URLClassLoader的findClass方法
// 动态加载类文件,解析成Class类,返回给调用方
return findClass(name);
}
}
从源码中我们可以看到,JSP类加载是先从JVM类缓存中(也就是Bootstrap等类加载器加载的类)加载,如果不是JSP的类,就从Web应用程序类加载器WebappClassLoader中加载,如果还未找到,则从指定的url路径下加载。
JasperLoader的初始化代码如下:
代码来源:apache-tomcat-10.1.13-src/java/org/apache/jasper/JspCompilationContext.java
public ClassLoader getJspLoader() {
if( jspLoader == null ) {
jspLoader = new JasperLoader(new URL[] {baseUrl}, getClassLoader(),
basePackageName, rctxt.getPermissionCollection());
}
return jspLoader;
}
在初始化JasperLoader时指定了加载路径和父加载器。
4.5 线程上下文类加载器
在前面的几节中,重点分析了双亲委派模型的实现原理,可以得出一个基本的结论:子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载器问题。例如,Java中提供了一些接口(Service Provider Interface,SPI),如JDBC、JNDI和JAXP等,这些接口类由BootstrapClassLoader或者PlatformClassLoader加载,但是这些接口的实现却是由第三方提供,一般是由AppClassLoader来加载的。而BootstrapClassLoader无法加载到核心接口的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。Java为了解决这个问题,引入了线程上下文类加载器(ContextClassLoader)加载。
与线程上下文加载器有关的内容在线程Thread类中,代码如下:
jdk11/src/java.base/share/classes/java/lang/Thread.java
public class Thread implements Runnable {
// 其他属性省去...
private ClassLoader contextClassLoader;
// 获取上下文加载器
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
return contextClassLoader;
}
// 设置上下文加载器
public void setContextClassLoader(ClassLoader cl) {
contextClassLoader = cl;
}
// 创建线程实例
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 创建线程实例的线程
Thread parent = currentThread();
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
// 初始化为父线程的上下文类加载器
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// 其他代码省去.......
}
}
从源码可以看处,线程上下文类加载器是线程Thread的一个属性,可以缓存当前线程使用的类加载器,在线程创建时继承父线程的上下文类加载器,线程运行时可以设置为其他值。
在JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。 而在JDK11中,DriverManager类在java.sql模块中,该模块由PlatformClassLoader加载,而依赖中的mysql驱动对应的类,由应用程序类加载器来加载,DriverManager在双亲委托模型下无法访问mysql驱动类,JDK在这里使用了上下文类加载器来绕开限制。
来看下DriverManager源码实现:
jdk11/src/java.sql/share/classes/java/sql/DriverManager.java
public static Driver getDriver(String url) throws SQLException {
// 加载驱动类
ensureDriversInitialized();
for (DriverInfo aDriver : registeredDrivers) {
try {
if (aDriver.driver.acceptsURL(url)) {
return (aDriver.driver); // 获取驱动
}
} catch(SQLException sqe) {
// ignore
}
}
throw new SQLException("No suitable driver", "08001");
}
ensureDriversInitialized从名称可以看出是确保驱动类正确的初始化,然后遍历已经注册的驱动,返回Driver驱动对象。先来看下ensureDriversInitialized方法是如何获取驱动类的。
private static void ensureDriversInitialized() {
if (driversInitialized) return;
synchronized (lockForInitDrivers) {
if (driversInitialized) return;
String drivers;
// 从系统环境变量中获取驱动类名称
try {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
} catch (Exception ex) {
drivers = null;
}
// 使用ServiceLoader从jar包中读取驱动的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
// 类加载
if (drivers != null && !drivers.equals("")) {
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
// 尝试在此加载类,如果SystemClassLoader加载失败,则驱动初始化失败
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
// ...
}
}
}
driversInitialized = true;
}
}
驱动类的加载,主要在ServiceLoader.load(Driver.class)
,load方法的实现如下:
jdk11/src/java.base/share/classes/java/util/ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
从源码可以看出,在具体加载类时使用了当前线程的上下文加载器加载类。
4.6 热加载与卸载
在类的加载过程中,我们知道会先检查该类是否已经加载,如果已经加载了,则不会从jar包或者路径上查找类,而是使用缓存中的类。JVM表示一个类是否是相同的类有两个条件:第一个是类的全限定名称是否相同,第二个是类的加载器实例是否是同一个。 因此要实现类的热加载,可以使用不同的类加载器来加载同一个类文件。使用不同的类加载器实例加载同一个类文件,随着加载次数增加,类的个数也会不断增加,如果不及时清理元空间/永久代,会有内存溢出的风险。 然而类卸载的条件非常苛刻,一般要同时具备下面的三个条件才可以卸载,并且需要JVM执行fullgc后才能完全清除干净。类卸载的三个条件如下:
图4-7 类卸载的条件
full GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。从上面的三个条件可以看出JVM自带的类加载器不会被回收,因此JVM的类不会被卸载。只有自定义类加载器才有卸载的可能。下面给出一个具体的需求,并使用热加载来完成。应用在运行时加载一个class脚本,class脚本可以做到热更新。有这样一个脚本接口,具有执行运算的功能。
public interface Script {
// 执行运算
String run(String key);
}
脚本的实现类,负责具体的计算功能。
public class ScriptImpl implements Script {
public ScriptImpl() {
}
public String run(String key) {
return key;
}
}
JVM运行过程中替换脚本的实现,即可以实现脚本的更新功能。
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader appClassloader = Main.class.getClassLoader();
ScriptClassLoader scriptClassLoader1 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl1 = scriptClassLoader1.loadClass("ScriptImpl");
System.out.println(scriptImpl1.hashCode());
ScriptClassLoader scriptClassLoader2 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl2 = scriptClassLoader2.loadClass("ScriptImpl");
// class对象不相同
assert scriptImpl1 != scriptImpl2;
}
}
使用不同的类加载器加载同一个类,得到的class对象不一样,运行时更新ScriptImpl类的实现即可。ScriptClassLoader的实现如下:
public class ScriptClassLoader extends ClassLoader {
private String classDir;
public ScriptClassLoader(String classDir,ClassLoader classLoader) {
super(classLoader);
this.classDir = classDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classDate = getClassByte(name);
if (classDate == null) {
return null;
}
return defineClass(name, classDate, 0, classDate.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private byte[] getClassByte(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classDir + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
in.close();
out.close();
}
return null;
}
}