【Tomcat】基础总结:类加载机制
【Tomcat】基础总结:类加载机制
- 1、Tomcat中有哪些类加载器?
- 2、Tomcat的类加载机制是怎么样的?
- 2.1 为什么破坏双亲委派
- 2.2 如何避免重复加载
1、Tomcat中有哪些类加载器?
Tomcat的类加载机制是指Tomcat在运行时如何加载和管理Java类。Tomcat的类加载机制的实现并没有严格遵守双亲委派原则,而是采用了一种层次化的类加载器结构,这种结构旨在提供更好的隔离性和灵活性,以支持多个Web应用程序的部署和运行。
默认情况下,Server类加载器和Shared类加载器是未定义的,需要通过在conf/catalina.properties中定义server.loader和/或shared.loader属性的值,才会是这个更复杂的层次结构。
Server类加载器只对Tomcat内部可见,对于Web应用程序完全不可见。
Shared类加载器对所有Web应用程序可见,可以用于在所有Web应用程序之间共享代码。但是,对这些共享代码进行更新将需要重新启动Tomcat。
所以,真正的需要一定有的,并且我们通常需要关注的就是下面这个层级关系:
启动类加载器(Bootstrap ClassLoader): 负责加载JVM自身的核心类库(如java.lang、java.util等)和JVM相关的类。启动类加载器是JVM的一部分,负责加载JVM运行所需的基础类。主要加载JRE中的lib包及lib/ext包下的内容
系统类加载器(System Class Loader):负载加载Tomcat内部的一些核心类库,这些类一般在$CATALINA_HOME/bin这个目录下, 一般包含bootstarap.jar、tomcat-juli.jar以及common-daemon.jar等。
公共类加载器(Common Class Loader): 负责加载Tomcat的公共类和库,位于$CATALINA_HOME/lib目录下的JAR文件。这些类库是Tomcat启动时加载的,是整个Tomcat实例中共享的类。
Web应用程序类加载器(Webapp Class Loader): 每个Web应用程序都有一个独立的Web应用程序类加载器,负责加载该Web应用程序的类和资源。它从CATALINAHOME/webapps/<webappname>/WEB−INF/classes目录和CATALINA_HOME/webapps/<webapp_name>/WEB-INF/classes目录和CATALINAHOME/webapps/<webappname>/WEB−INF/classes目录和CATALINA_HOME/webapps/<webapp_name>/WEB-INF/lib目录加载类和JAR文件。
2、Tomcat的类加载机制是怎么样的?
Tomcat的类加载机制,在默认情况下,是先把当前要加载的类委托给BootstrapClassLoader尝试加载,为了避免JRE中的核心类被我们应用自己的类给覆盖(如String等),Bootstrap如果无法加载,那么就由WebAppClassLoader尝试加载,如果无法加载,那么再委托通过双亲委派的方式向上委派给Common、System等类加载进行加载,即顺序为:Bootstrap->WebApp->System->Common
上面的是默认情况,tomcat中有一个配置delegate,他的默认值是false,如果设置成true了,那么他就会严格遵守双亲委派,按照Bootstrap->System->Common->WebApp的顺序进行加载。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {//加锁,防止并发synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {if (log.isDebugEnabled()) {log.debug("loadClass(" + name + ", " + resolve + ")");}Class<?> clazz = null;// ...// 检查本地缓存是否已加载该类,如果是,则直接返回缓存中的 Class 对象。clazz = findLoadedClass0(name);if (clazz != null) {if (log.isDebugEnabled()) {log.debug(" Returning class from cache");}if (resolve) {resolveClass(clazz);}return clazz;}// 检查另一个类加载缓存,如果是GraalVM环境,直接返回缓存中的 Class 对象。clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);if (clazz != null) {if (log.isDebugEnabled()) {log.debug(" Returning class from cache");}if (resolve) {resolveClass(clazz);}return clazz;}/** 尝试使用Bootstrap类加载器加载类,以防止Web应用程序覆盖Java SE类。如果加载成功,则返回加载的 Class 对象。*/String resourceName = binaryNameToPath(name, false);ClassLoader javaseLoader = getJavaseClassLoader();boolean tryLoadingFromJavaseLoader;try {URL url = javaseLoader.getResource(resourceName);tryLoadingFromJavaseLoader = url != null;} catch (Throwable t) {ExceptionUtils.handleThrowable(t);tryLoadingFromJavaseLoader = true;}if (tryLoadingFromJavaseLoader) {try {clazz = javaseLoader.loadClass(name);if (clazz != null) {if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}}boolean delegateLoad = delegate || filter(name, true);// 根据 delegate 属性和其他条件判断是否应该委派加载给父类加载器。// 如果需要委派,则直接先进行委派if (delegateLoad) {if (log.isDebugEnabled()) {log.debug(" Delegating to parent classloader1 " + parent);}try {clazz = Class.forName(name, false, parent);if (clazz != null) {if (log.isDebugEnabled()) {log.debug(" Loading class from parent");}if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}}// 自己尝试加载// 能走到这里,肯定是BootStrap没加载到,之后还有两种情况:// 1、如果delegate为ture的话,说明上层类加载器也没记载到。// 2、如果delegate为false,那么就还没有进行过委派,先在这里尝试自己加载。if (log.isDebugEnabled()) {log.debug(" Searching local repositories");}try {clazz = findClass(name);if (clazz != null) {if (log.isDebugEnabled()) {log.debug(" Loading class from local repository");}if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}// 如果delegate为false,说明还没有做过委派,那么委派给父类加载器加载类。if (!delegateLoad) {if (log.isDebugEnabled()) {log.debug(" Delegating to parent classloader at end: " + parent);}try {clazz = Class.forName(name, false, parent);if (clazz != null) {if (log.isDebugEnabled()) {log.debug(" Loading class from parent");}if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}}}throw new ClassNotFoundException(name);
}
整个代码的过程就是:
- 加锁: 方法使用同步块确保线程安全
- 检查已加载类缓存: 首先,通过调用 findLoadedClass0 方法检查本地缓存是否已加载该类,如果是,则直接返回缓存中的 Class 对象。
- 检查已加载类缓存(GraalVM 兼容性处理): 通过调用 findLoadedClass 方法检查另一个类加载缓存,如果是GraalVM环境,直接返回缓存中的 Class 对象。
- 尝试使用Bootstrap类加载器加载: 尝试使用Bootstrap类加载器加载类,以防止Web应用程序覆盖Java SE类。如果加载成功,则返回加载的 Class 对象。
- 决定是否委派加载: 根据 delegate 属性和其他条件判断是否应该委派加载给父类加载器。
- 委派给父类加载器: 如果需要委派加载(delegate为true),尝试使用父类加载器加载类。
- 自己尝试加载: 如果未指定需要委派(delegate为false),或者未从父类加载器中找到类,则调用 findClass 方法尝试自己进行类加载。
- 委派给父类加载器: 如果未指定需要委派(delegate为false),且自己没加载到类,则尝试使用父类加载器加载类。
2.1 为什么破坏双亲委派
一个Tomcat,是可以同时运行多个应用的,而不同的应用可能会同时依赖一些相同的类库,但是他们使用的版本可能是不一样的,但是这些类库中的Class的全路径名因为是一样的,如果都采用双亲委派的机制的话,是无法重复加载同一个类的,那么就会导致版本冲突。
而为了有更好的隔离性,所以在Tomcat中,每个应用都由一个独立的WebappClassLoader进行加载,这样就可以完全隔离开。而多个WebAppClassLoader之间是没有委派关系的,他们就是各自加载各自需要加载的Jar包。
由于每个Web应用程序都有自己的类加载器,因此不同Web应用程序中的类可以使用相同的类名,而不会产生命名冲突。
同时,由于每个Web应用程序都有自己的类加载器,因此在卸载一个Web应用程序时,它的所有类都会从内存中清除,这可以避免内存泄漏的问题。
这种层次化的类加载器结构和委派机制确保了类的唯一性和隔离性,避免了类的重复加载和冲突,同时也实现了多个Web应用程序的隔离和独立运行。
2.2 如何避免重复加载
因为每个应用都是用WebAppClassLoader独自加载的,但是如果有一个公共的jar包,比如Spring,各个应用的版本都一样,那么岂不是要重复加载很多次了?这不是浪费么?
Tomcat给了个方案,那就是SharedClassLoader,我们可以把可以指定一个目录,让SharedClassLoader来加载,他加载的类在各个APP中都是可以共享使用的。
参考链接:
1、https://www.yuque.com/hollis666/wk6won/rgupmyr7wo4s8zi0
2、https://www.yuque.com/hollis666/wk6won/evlwzsa8s6mx93ly