Tomcat类加载器揭秘:“重塑”双亲委派模型
在Java世界中,类加载器作为程序运行时动态加载类的基石,遵循着经典的双亲委派模型原则,这一设计确保了类的唯一性和安全性
然而,在某些特殊应用场景下,如应用服务器领域,传统的双亲委派模型需要被巧妙地“重塑”以满足更复杂的需求
Apache Tomcat,作为最流行的Java Web应用服务器之一,正是这样一个打破常规、挑战传统的典范
本文,我们将踏上一段深度探索之旅,揭秘Tomcat如何以及为何要打破Java的双亲委派模型
双亲委派模型
先来复习下类加载器相关知识(也可以查看类加载器文章):
JVM运行时遇到类需要检测类是否加载,如果未加载则将类信息加载到运行时的方法区并生成Class对象
在这个过程中,JVM通过类加载器进行类加载
类加载器分为引导(Bootstrap)、扩展(Ext)、应用(App)类加载器(ClassLoader)
引导类加载器由C 实现,用于加载核心类库
扩展类加载器用于加载扩展库,应用类加载器则常用于加载我们自定义的类
扩展、应用类加载器由Java代码实现,组合为父子关系(不是继承)
默认情况下类加载会使用双亲委派模型:进行类加载时将类交给父类尝试加载,如果父类不加载再由自己加载,当自己也无法加载时抛出ClassNotFoundException异常
双亲委派模型下类加载的顺序为:引导 Boot -> 扩展 Ext -> 应用 App
代码语言:java复制ClassLoader.loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁保证类加载
synchronized (getClassLoadingLock(name)) {
//检查类是否加载
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) {
}
//父类没有加载,由自己加载
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;
}
}
如果我们编写一个全限定类名相同的核心类库时,比如java.lang.Object,并调用其中的main方法时,程序会报错
代码语言:java复制错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
这是因为双亲委派模型会防止java.lang.Object这种核心类库被篡改,它们由父类加载器进行加载,因此加载时找不到我们编写的main方法
Tomcat类加载器
既然双亲委派模型能够防止核心类库被篡改,那么Tomcat为啥还要打破双亲委派模型呢?
在Tomcat中(Servlet规范),允许多Web应用(多context容器)
如果多Web应用下依赖的类名相同但这两个类不是同一个类(功能不同),该怎么办?又或者说依赖的三方类,类名相同但版本不同该怎么办?
而有些类又需要Web应用(context容器)共享该怎么办?
通过类加载器可以解决隔离的问题,不同类加载器加载的类,即使全限定类名相同那它们也不是同一个类
因此在JVM中,判断类是否相同,必须全限定类名相同且类加载器相同
为了解决这些问题,Tomcat需要使用自定义类加载器对类进行隔离
前文21张图解析Tomcat运行原理与架构全貌介绍过Context容器中有一个Loader组件,它就是Tomcat在Context容器中的类加载器
Tomcat使用WebAppClassLoader对应每个Context容器下的Loader,来进行容器间类的隔离
而如果容器间需要共享相同的类,再增加个共享的类加载器SharedClassLoader作为WebAppClassLoader的父类
还要其他类似隔离的类加载器就不再说了(一层不够就再加一层)
源码解析
在Tomcat启动容器时,会启动后台定时检查的任务
代码语言:java复制ContainerBase.threadStart
protected void threadStart() {
if (backgroundProcessorDelay > 0
//...
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
//执行定时任务
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
}
后台定时检查的任务使用JUC下做定时任务的线程池ScheduledExecutorService.scheduleWithFixedDelay
其中ContainerBackgroundProcessor为定时检查任务,它会从顶级容器开始依次让容器中管理的组件执行backgroundProcess方法
其中Context容器中的Loader组件用于类加载,在backgroundProcess方法中,如果检查到有更新,则会重新加载容器context.reload()
代码语言:java复制WebappLoader.backgroundProcess
@Override
public void backgroundProcess() {
if (reloadable && modified()) {
try {
//设置线程上下文中的类加载器
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
//重新加载容器
context.reload();
}
} finally {
if (context != null && context.getLoader() != null) {
//结束把类加载器重新设置回来
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}
StandardContext.reload
在Context容器reload方法中,先暂停卸载子组件,再注册启动子组件,在此过程中需要停止接收请求
代码语言:java复制public synchronized void reload() {
//组件不可用抛出异常
if (!getState().isAvailable()) {
throw new IllegalStateException
(sm.getString("standardContext.notStarted", getName()));
}
if(log.isInfoEnabled()) {
log.info(sm.getString("standardContext.reloadingStarted",
getName()));
}
//标记 暂停接收请求
setPaused(true);
try {
//停止组件
stop();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.stoppingContext", getName()), e);
}
try {
//启动组件
start();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.startingContext", getName()), e);
}
//标记 不再暂停接收请求
setPaused(false);
if(log.isInfoEnabled()) {
log.info(sm.getString("standardContext.reloadingCompleted",
getName()));
}
}
在stop暂停组件,最终会调用生命周期中的stopInternal去组织停止、销毁容器中使用到的组件
StandardContext.stopInternal
卸载子组件的类前,需要把当前线程的类加载器切换为当时创建的(Loader的类加载器),卸载完又换回来,在这个过程中对应绑定/解绑
组织停止后台线程、子组件、过滤器、管理器、pipeline等容器中使用的组件,最终reset清理context容器
代码语言:java复制protected synchronized void stopInternal() throws LifecycleException {
//设置停止状态触发事件
setState(LifecycleState.STOPPING);
//绑定类加载器(方便卸载子组件)
ClassLoader oldCCL = bindThread();
try {
//获取子组件
final Container[] children = findChildren();
//停止后台运行线程
threadStop();
//停止子组件
for (Container child : children) {
child.stop();
}
//停止过滤器
filterStop();
//停止管理器
Manager manager = getManager();
if (manager instanceof Lifecycle && ((Lifecycle) manager).getState().isAvailable()) {
((Lifecycle) manager).stop();
}
//停止监听器
listenerStop();
//...
//停止pipeline
if (pipeline instanceof Lifecycle &&
((Lifecycle) pipeline).getState().isAvailable()) {
((Lifecycle) pipeline).stop();
}
//停止其他资源...
} finally {
//卸载完 解绑,当前线程的类加载器变回原来的
unbindThread(oldCCL);
}
// reset 容器
context = null;
try {
resetContext();
} catch( Exception ex ) {
log.error( "Error resetting context " this " " ex, ex );
}
}
在卸载类的过程中,会使用当前context容器下的类加载器去进行卸载
后续start启动再新创建context容器中使用到的组件,其中类加载器流程总结如下:
WebappClassLoaderBase.loadClass
- 检查类是否加载
- 拿到扩展类加载器调用(先引导、再扩展,防止核心类库被破坏)
javaseLoader = getJavaseClassLoader()
javaseLoader.loadClass(name)
(这里扩展类交给引导类进行加载,还是以前双亲委派模型代码)
- 当前类加载器尝试类加载
findClass(name)
(这里可能交给父类加载,比如之前说过的共享的SharedClassLoader) - 应用类加载器尝试加载
Class.forName(name, false, parent)
- 抛出异常
throw new ClassNotFoundException(name)
实际上Tomcat就是把当前类加载器尝试加载的时机放到应用类加载器前,还是引导、扩展类加载优化加载(防止核心类库被破坏)
总结
双亲委派模型优先将类交给父类加载,如果父类不能加载再由自己加载,当自己也无法加载时抛出ClassNotFoundException异常,能够保证核心类库不被破坏
通过类加载器可以解决隔离的问题,判断类是否相同时要满足全限定类名和类加载器都相同
Tomcat为了解决多Web应用间类的隔离,自定义WebAppClassLoader类加载器作为Context容器的Loader
WebAppClassLoader类加载流程先检查类加载,优先使用引导、扩展类加载器,再尝试自己的父类/自己进行加载,最后在尝试让应用类加载器加载,都无法加载抛出异常