JVM的类加载机制
JVM默认内置3种类加载器,分别是:
1.BootstrapClassLoader
引导类加载器,由c++实现,用于加载java的核心类库,如rt.jar,java环境下无法获取其引用。
默认加载的类路径由系统变量sun.boot.class.path指定,默认是JRE_HOME/lib,可以通过两种方式指定引导的类路径:
1. 使用-Xbootclasspath指定要加载的核心类库如:
-Xbootclasspath:"JRE_HOME\lib\resources.jar;JRE_HOME\lib\rt.jar;JRE_HOME\lib\sunrsasign.jar;JRE_HOME\lib\jsse.jar;JRE_HOME\lib\jce.jar;JRE_HOME\lib\charsets.jar;JRE_HOME\lib\jfr.jar;"
2.使用-D参数指定系统变量sun.boot.class.path的值来告诉核心类库的路径,如:
-Dsun.boot.class.path="JRE_HOME\lib\resources.jar;JRE_HOME\lib\rt.jar;JRE_HOME\lib\sunrsasign.jar;JRE_HOME\lib\jsse.jar;JRE_HOME\lib\jce.jar;JRE_HOME\lib\charsets.jar;JRE_HOME\lib\jfr.jar;"
注:以上两种方式指定jar时必须提供完整的jar路径,不支持直接配置目录,且多个路径必须使用“系统路径分隔符”分隔。
2.ExtClassLoader
扩展类加载器,用于加载核心类库之外的扩展类库,加载路径由系统属性java.ext.dirs指定, 默认在JRE_HOME/lib/ext下。
例如对于jce的实现,sun提供了sunjce_provider.jar,如果一些高级的加密算法jdk还没有很好支持,可以使用bcp的实现,只需将bcprov-ext-jdk15on-160.jar拷贝到JRE_HOME/lib/ext下即可。扩展目录可以手动指定:
-Djava.ext.dirs="JRE_HOME\lib\ext;D:/ext"
注:与sun.boot.class.path不同,这里支持配置目录,系统会自动搜索目录下所有jar包和class文件,多个目录使用“系统路径分隔符”分隔。
2.AppClassLoader
应用(或系统)类加载器,用于加载用户自定义的类,实例化时通过以下3中途径构建AppClassloader:
1. 使用Java -classpath选项指定
2. 通过系统属性-Djava.class.path指定
3. 通过环境变量classpath指定
4. 以上都没设置则默认运行类所在的当前目录
检索顺序为1,2,3。一旦检索的相关选项,则不再继续检索,如:使用了-classpath选项,那么-D和环境变量方式将不再起效。
类加载机制
BootstrapClassloader
|
ExtClassloader
|
AppClassloader
/ \
CustomLoader1 CustomLoader2 ...
jdk类的加载默认使用双亲委派模式,即当一个类加载器收到了类加载请求时,它会将请求委派给父(parent)加载器加载,所有的加载请求最终都被传送到顶层的BootstrapClassloader类加载器中。只有在顶层父加载器无法加载该类时再向下从自己的类路径中查找。
说明:
1.使用AppClassloader加载java.lang.String,最终由BootstrapClassloader进行读取装载,那么BootstrapClassloader就是String的定义类加载器,那么String的Class对象的getClassLoader()方法返回的定义类加载器就应该是BootstrapClassloader,但因为BootstrapClassloader是c++写的,java中无法获取其引用,因此getClassLoader()返回null,其它情况下返回定义的类加载器实例。
2. 自定义的classloader一般直接或间接继承自ClassLoader类,通过实现findClass从我们希望的路径中寻找class文件,但最终须通过ClassLoader的defineClass(final方法无法覆盖)请求jvm加载我们的类,默认的loadClass方法使用双亲委派方式加载类,如果不想使用双亲委派,需要重写loadClass,但无论如何,java核心类库的加载最终都应该由ExtClassloader,BootstrapClassloader负责。
3.同一个类加载器实例只能加载类一次,如果强行加载载多次,会报连接错误,但多个类加载器(即便是同一类型的实例)实例可以分别加载相同的类。
4.由子加载器定义(层级关系parent,不是继承)的类可以访问父加载器定义的类,但由父加载器定义的类无法直接访问由子加载器定义的类。例如由AppClassloader定义的类A访问了一个只能由Custom1Loader(如上图)定义的类B,在A中访问B时默认会使用AppClassloader加载B,由于双亲委派机制,委托给父ExtClassloader直到BoosstrapClassloader,最终因为这3个加载器都无法载B而报错。
同理由CustomLoader1定义的类A也无法直接访问由CustomLoader2定义的类B,因为CustomLoader1在加载A时访问了类B,此时CustomLoader1会尝试用自己加载B,最终加载失败。那么如何才能访问B?可以用反射,如:
//由CustomLoader1定义的类A,其方法f访问由CustomLoader2定义的B
public void f() {
//这里不能直接暴露B的类型,否则会报类找不到,只能使用反射api调用B的方法
Object bInstance = CustomLoader2.loadClass("B").newInstance();
System.out.print(bInstance.toString());
}
通过反射无法知道被调用类的具体类型和被调用的方法,因此也就无从调用类的方法,那么该如何在类A中显式调用B类的方法?
可以声明一个接口C,C接口必须能由CustomLoader1与CustomLoader2共同的父加载器定义(如上图的AppClassloader),此时让B类去实现接口C,伪代码如下:
//C必须能由CustomLoader1与CustomLoader2共同的父加载器如AppClassloader定义
public interface C {
public void f();
}
//类A由Custome1Loader定义
public class A {
public static void main(String[] args) throws Exception {
//B由CustomLoader2定义
//没有显式暴露B的类型,但可以暴露C,使用强制类型转换
C bInstance = (C)(CustomLoader2.loadClass("B").newInstance());
bInstance.f();//成功通过接口方法调用了B的f方法
}
}
public class B implements C {
@Override
public void f() {
}
}
5.同一个class文件交给不同的ClassLoader加载产生的Class对象是不相等的(==或equals都为false),它们彼此独立,包括类上的静态属性,且类型无法兼容,例如两个不同加载器创建的User对象无法直接互通,如User u1由CustomLoader1定义,User u2由CustomLoader2定义,对于u1=u2这样的赋值是不允许也是无法强转的,因为类型不兼容。
试想下:自定义一个java.lang.String,并自定义一个类加载器CustomLoader3,打破双亲委派,先从本地路径加载搜索自定义的String,这样是否能对系统进行攻击?
其实是行不通的,因为Classloader的defineClass在加载用户自定义类之前发现包名以java开头会抛出SecurityException异常。即使可以成功加载了自定义的String,但java核心的类在需要一个String类型时无法访问到自定义String,它会委托bootstrapClassloader类加载器加载javase的String,于是整个系统会出现两个相同签名但不同类型的String对象(一个是bootstrap加载的String,一个是CustomLoader3加载的String),这样也可能导致错误发生。
6.创建一个新线程时,此线程的线程上下文类加载器自动继承父线程的上下文类加载器。
Tomcat类加载机制
tomcat默认提供5中类型的类加载器,如图:
BootstrapClassloader
|
ExtClassloader
|
AppClassloader
|
commonLoader
|
_______________________________
| |
| |
catalinaLoader sharedLoader
/ \
/ \
webappLoader1 webappLoader2 ...
/ \
/ \
jasperloader1 jasperloader2 ...
1. commonLoader (父为AppClassloader)
tomcat服务器最顶层的类加载器,加载路径在文件catalina.properties中配置,由配置项“common.loader“指定,
被tomcat服务器本身和web应用共享,如servlet接口规范包如servlet-api.jar、jsp-api.jar,tomcat作为servlet容器的标准实现必然需要依赖这些包,而我们的应用需要实现我们自己的servlet也需要依赖这些包,这种情况将servlet-api.jar、jsp-api.jar放到common.loader指定的路径下由commonLoader加载是最好的选择。
2. catalinaLoader(父为commonLoader )
Tomcat容器私有的类加载器,用于加载框架类本身,加载路径中的类对于Webapp不可见,加载路径在文件catalina.properties中配置,由配置项“server.loader“指定。加载诸如:catalina.jar(核心类库,包含启动入口类)、tomcat-coyote.jar(对socket字节流的支持)等。
3. sharedLoader(父为commonLoader )
被各个web应用共享的类加载器,加载路径中的class对于所有web应用可见,由于与catalinaLoader平级,因此tomcat框架核心类对应用不可见,加载路径在文件catalina.properties中配置,由配置项“shared.loader“指定。
例如一些jdbc数据库的连接驱动被所有web应用共享,可以由它加载,同时也可以避免tomcat jdbc内存泄漏问题。
4. webappLoader(父为sharedLoader)
web应用私有的类加载器,一般用于加载应用自身/WEB-INF/classes下的class文件和/WEB-INF/lib目录下的jar
5. jasperLoader(父为webappLoader)
用于加载由jsp编译生成的servlet,默认路径为${catalina.base}/work/Catalina/localhost/
Catalina为service.xml下service的名称,localhost为Host节点的名称。
注:
需要注意的是自tomcat6以后,在catalina.properties中默认只配置了“common.loader“,用于实例化commonLoader,而其它两个选项
“server.loader“与“shared.loader“默认是空,因此这两个类加载器不会单独创建实例,而是使用共同的实例:commonLoader,即
commonLoader 、catalinaLoader、sharedLoader三者是同一个实例对象,都指向commonLoader ,因此tomcat6之后默认的层次图如下:
BootstrapClassloader
|
ExtClassloader
|
AppClassloader
|
commonLoader
/ \
/ \
webappLoader1 webappLoader2 ...
/ \
/ \
jasperloader1 jasperloader2 ...
java应用程序的入口是由“main“方法开始的,而main方法所在的类只能由AppClassloader类加载器加载,AppClassloader
通常从CLASSPATH
环境变量或java.class.path系统属性或-classpath选项指定的路径中加载所需的类。所有这些类对于Tomcat内部类和Web应用程序都是可见的。但是,标准的Tomcat启动脚本($CATALINA_HOME/bin/catalina.sh
或 %CATALINA_HOME%\bin\catalina.bat
)完全忽略CLASSPATH
环境变量本身的内容,而是从以下存储库构建AppClassloader:
-
$ CATALINA_HOME / bin / bootstrap.jar — 包含用于初始化Tomcat服务器的main()方法位于Bootstrap类,以及tomcat内部依赖的类加载器实现类(ClassLoaderFactory工厂类)。
-
$ CATALINA_BASE / bin / tomcat-juli.jar或 $ CATALINA_HOME / bin / tomcat-juli.jar —记录日志的实现类。其中包括对
java.util.logging
API的增强类 ,称为Tomcat JULI。如果
tomcat-juli.jar
出现在 $ CATALINA_BASE / bin中,会自动覆盖 $ CATALINA_HOME / bin中的同名jar。在某些日志记录配置中很有用 -
$ CATALINA_HOME / bin / commons-daemon.jar — Apache Commons Daemon项目中的类,好像是将java程序以一个服务运行在系统上,有兴趣的自己研究下。
前面提到,tomcat框架类的加载不是交由catalinaLoader来完成的么?没错,tomcat的启动类有两个,一个是Bootstrap类包含了main方法,只能由AppClassloader加载,另一个是Catalina类,对于tomcat的的启动停止(实则调用Bootstrap的start、stop)其实都是将调用委托给Catalina类实现的,也就是所Catalina类才是tomcat的真正入口类。Bootstrap做了哪些事?:
1.负责创建tomcat的3个核心类加载器catalinaLoader,commonLoader,sharedLoader由AppClassloader加载。
2.通过catalinaLoader创建Catalina类,并将对Bootstrap发起的调用都转发给Catalina,由于Catalina由catalinaLoader加载,因此
Catalina依赖的所有框架类,默认的类加载器也变成了catalinaLoader。
因为catalinaLoader并不在web应用程序加载器的层次中,因此应用也就无法直接访问tomcat核心类
应用类加载器webappLoader
与jvm的委派模式不同,webappLoader在加载应用类时并没有直接委托给父加载器加载,通过前面的讲解我们知道,一个类加载器只能加载类一次,如果我委托给父加载器加载,比如AppClassloader,而AppClassloader在虚拟机启动的时候有且仅有一个实例,对于多个应用使用相同的类,如App1和App2都用到了log4j的类,是无法同时加载,势必导致应用启动失败,另外tomcat支持热加载,修改一个class文件,在不启动应用的情况下重新加载修改后的类,单实例的AppClassloader是无法加载两次的,除非AppClassloader可以先卸载之前就的Class再加载新的,不过很遗憾,jvm没有提供支持,对于一个类的卸载,仅当没有任何引用指向一个ClassLoader时,此Classloader和其加载的类才会被卸载。而AppClassloader是不可能被卸载的。要解决此问题只能自己实现类加载器,从自己的本地仓库加载类,在有必要的情况下销毁自定义的Classloader并重新创建,来实现热加载功能。
加载类步骤如下:
1.从自身的缓存中查找,webappLoader维护一个map缓存已加载的类
2.如果自身缓存中没找到,则从虚拟机的缓存中查找
3.如果没有再委托给ExtClassloader、BootstrapClassloader查找,这样javase的核心类就可以仍由核心加载器加载
4.如果还没找到再从本地挂载路径/WEB-INF/classes,与WEB-INF/lib中查找
5.如果还没找到,再委托给sharedLoader加载(sharedLoader委托给commonLoader,再委托给AppClassloader。。。)
6.还没找到,抛出ClassNotFoundException异常
注:tomcat针对Loader元素提供了delegate属性,用于控制是否启用java的委派模式,默认是false,如果设成true,那么第4、5的搜索顺序进行颠倒,即按:1,2,3,5,4,6进行搜索,实际上在步骤3之后tomcat发现要加载的类是servlet规范相关的api,包名以如下包开头时,delegate将隐式为true,也会默认按1,2,3,5,4,6搜索:
javax.el.、javax.servlet.、javax.websocket.、javax.security.auth.message.、
org.apache.jdbc.、org.apache.catalina.、org.apache.jasper.、org.apache.juli.、
org.apache.tomcat.、org.apache.naming.、org.apache.coyote.
JasperLoader
用于加载由jsp编译后的servlet,每个jspservlet由一个JasperLoader加载,纯一对一关系,为什么是一个JasperLoader每次只加载一个jspservlet?是为了支持热加载,我们经常在不重启动tomcat的情况下修改jsp也能生效。一旦文件被修改,tomcat会将旧的JasperLoader丢弃(没有引用再指向旧的JasperLoader),以达到销毁的目的,然后再创建一个新的JasperLoader实例用于加载修改后的jspservlet进行服务。JasperLoader的父加载器是webappLoader,默认搜索路径如下:
1.从虚拟机的缓存中查找
2.没找到,查看包名是不是以org.apache.jsp开头,如果是,则从tomcat临时目录work目录下查找(默认tomcat会将jsp编译成servlet并放到work目录下,包名以org.apache.jsp开头)
3.如果包名不以org.apache.jsp开头,则委托给webappLoader进行查找
4.找不到,抛出ClassNotFoundException异常
注:为啥要检查是否以org.apache.jsp开头?因为你编译后的jspservlet可能要到了其它类,由于classloader的继承,这些类的加载默认也会使用相同的JasperLoader进行加载,当然需要直接委托给webappLoader。
java endorsed覆盖机制
Java SE运行时环境可以使用自定义JAR文件中的类来覆盖Java核心类。比如jdk某个类存在bug,但又不想升级jdk,你只需要提供相同签名的不同实现版本,然后打成jar包并在启动程序时使用-Djava.endorsed.dirs来设置你自定义jar包路径即可。而对于tomcat你需要将相关jar包放到$CATALINA_HOME/endorsed目录下即可。
不过endorsed机制不能覆盖java.lang包下的类,通常是在jdk一些提供xml解析的包版本比较旧时,可以通过这种方式使用新版本的包进行覆盖,详细参考官方文档https://docs.oracle.com/javase/8/docs/technotes/guides/standards/,值得注意的是oracle并不推荐使用此特性,因为此特性可能在未来版本中删除。
自定类查找路径
默认情况下应用类的加载时的搜索路径是$WebRoot/WEB-INF/classes和$WebRoot/WEB-INF/lib中,如果要添加额外的路径需要在Context的Resources节点下新增如下配置:
1. 前置扩展/WEB-INF/classes目录使用DirResourceSet
加载类时,在默认$WebRoot/WEB-INF/classes之前搜索:D:/external/classes下的class文件,如果能找到将不再搜索$WebRoot/WEB-INF/classes:
<PreResources base="D:/external/classes"
className="org.apache.catalina.webresources.DirResourceSet"
webAppMount="/WEB-INF/classes"/>
注:base指明额外的class文件存在于D:/external/classes目录下,className使用org.apache.catalina.webresources.DirResourceSet代表base指定的路径是一个目录,使用webAppMount="/WEB-INF/classes"代表是对"/WEB-INF/classes"的扩展。
2. 前置扩展/WEB-INF/classes目录使用JarResourceSet
加载类时,在默认$WebRoot/WEB-INF/classes之前搜索:D:/external/mylib.jar下的class文件,如果能找到将不再搜索$WebRoot/WEB-INF/classes:
<PreResources base="D:/external/mylib.jar"
className="org.apache.catalina.webresources.JarResourceSet"
webAppMount="/WEB-INF/classes"/>
注:base指明额外的class文件存在于D:/external/mylib.jar包下,className使用org.apache.catalina.webresources.JarResourceSet代表base指定的路径是jar,使用webAppMount="/WEB-INF/classes"代表是对"/WEB-INF/classes"的扩展。
3. 前置扩展/WEB-INF/classes目录使用FileResourceSet
在默认在$WebRoot/WEB-INF/classes之前搜索:如果要加载test.MyServletContextListener那么从文件D:/external/test/MyServletContextListener.class进行加载
<PreResources base="D:/external/test/MyServletContextListener.class"
className="org.apache.catalina.webresources.FileResourceSet"
webAppMount="/WEB-INF/classes/test/MyServletContextListener.class"/>
注:base目标文件的位置,className使用org.apache.catalina.webresources.FileResourceSet代表base是一个文件,使用webAppMount="/WEB-INF/classes/test/MyServletContextListener.class"代表是对"/WEB-INF/classes"的扩展, 之后的test/MyServletContextListener.class表示当加载test.MyServletContextListener时从base指定的class文件加载此类。
4. 前置扩展/WEB-INF/lib目录使用DirResourceSet
加载类时,在默认$WebRoot/WEB-INF/lib之前搜索:D:/external/lib下的jar文件,如果能找到将不再搜索$WebRoot/WEB-INF/lib:
<PreResources base="D:/external/lib"
className="org.apache.catalina.webresources.DirResourceSet"
webAppMount="/WEB-INF/lib"/>
注:base指明额外的jar文件存在于D:/external/lib目录下,className使用org.apache.catalina.webresources.DirResourceSet代表base指定的路径是一个目录,使用webAppMount="/WEB-INF/lib"代表是对"/WEB-INF/lib"的扩展。
5. 前置扩展/WEB-INF/lib目录使用FileResourceSet
加载类时,在默认$WebRoot/WEB-INF/lib之前搜索:D:/external/mylib.jar下的class文件,如果能找到将不再搜索$WebRoot/WEB-INF/lib:
<PreResources base="D:/external/mylib.jar" <!--这里了必须是已存在的文件路径,不能是目录-->
className="org.apache.catalina.webresources.FileResourceSet"
webAppMount="/WEB-INF/lib/mylib.jar"/>
注:base指明额外的jar文件的路径,className使用org.apache.catalina.webresources.FileResourceSet代表base指定的路径是一个文件,使用webAppMount="/WEB-INF/lib/mylib.jar"代表是对"/WEB-INF/lib"的扩展, 之后的mylib.jar是文件名。
有人可能会有疑问,mylib.jar明明是一个jar包为啥不能用JarResourceSet,因为JarResourceSet只能对挂载路径/WEB-INF/classes进行扩展。
除了前置PreResources还有后置PostResources,以及MainResources、ClassResources、JarResources(详见http://imgs.hrm.cn/docs/config/resources.html):
搜索顺序依次为:
- PreResources
- MainResources
- ClassResources
- JarResources
- PostResources
MainResources与ClassResources由tomcat内部维护,无法通过xml配置进行指定,tomcat启动时会将$WebRoot/WEB-INF/lib以及我们手动挂载到/WEB-INF/lib下的jar包自动添加到ClassResources。另外也会将$WebRoot添加到MainResources中,默认从$WebRoot/WEB-INF/classes加载类也是由MainResources处理的。
应用热加载
热加载指的是修改一个应用的类或配置文件,在不重启tomcat的情况下能够使修改起效,例如修改了一个servlet的某个执行方法,tomcat会如何让修改起效?很简单,将当前已加载的这个servlet类进行卸载再重新加载不就行了?没错确实可以这么干,但是前面说过,一个类加载器实例无法直接重新加载一个类(或许java.lang.instrument包下的api可以,但并不是所有情况都可以支持重新加载,如仅修改了方法体代码的类可以重新加载,但如果删减了方法可能就不行了),但是可以重新创建一个新的Classloader进行加载,tomcat会使用一个新的WebappClassLoader重新加载所有类,而不只是加载被修改的那个类。旧的WebappClassLoader需要进行销毁,而所谓的销毁就是清除一切指向旧classLoader的引用,好让垃圾回收器回收旧的classLoader和其加载的类。
tomcat会开启一个监控线程,用于监听应用class文件和应用配置文件(如web.xml)的变化,一旦发生修改就会重启当前应用(先stop再start)。默认配置下不启用热加载,可以配置Context节点指定属性reloadable="true"开启。需要注意的是,对web.xml的修改可以响应热加载,但对于class文件的修改仅当class文件指向的类已被jvm加载了,才会响应热加载。如定义了一个MyServlet处理http请求,但自应用启动后一直没有请求过此servlet,此时MyServlet的类还没有被jvm加载过,此情况下修改MyServlet.class文件不会触发重加载。
停止一个应用的流程如下,每一个应用对应一个java实现类StandardContext:
1.触发监听器MapperListener的before_stop事件进行StandardContext的取消注册,执行unregisterContext(StandardContext)方法
2.停止StandardContext创建的BackgroundProcessor(定时检测线程)
3.销毁应用创建的servlet实例:
a. 触发MapperListener的before_stop事件取消注册Servlet实例,unregisterWrapper(StandardWrapper)
b. 执行servlet的destroy方法
4.销毁filter
执行filter的destroy方法
5.销毁当前应用创建的Session对象
a. 钝化session对象,检索session保存的属性,如果键值对的值类型实现了javax.servlet.httpHttpSessionActivationListener接口,则执行sessionWillPassivate回调方法
b. 将应用创建的session实例序列化到文件(默认文件名:$CATALINA_BASE\work\Catalina\localhost\[contextName]\SESSIONS.ser),序列化前会检索session保存的属性,发现键值对的值没有实现Serializable接口,会从session属性中移除此key并触发回调方法javax.servlet.HttpSessionAttributeListener.attributeRemoved,如果属性值实现了javax.servlet.HttpSessionBindingListener接口还会继续触发javax.servlet.HttpSessionBindingListener.valueUnbound方法调用。
c. 将session对象设置为过期expire(注:在应用停止时expire不会执行HttpSessionListener.sessionDestroyed方法,其它情况下可以)。
6.销毁应用监听器
调用ServletContextListener的contextDestroyed方法
7.停止NamingResources
8.执行ContextConfig的configureStop方法(configure_stop事件)
9.移除servletContext的保存属性键值对,并触发执行javax.servlet.ServletContextAttributeListener的attributeRemoved方法
10.执行Realm的stop方法
11.销毁应用类加载器WebappClassLoader并清除引用:
a. 清除DriverManager的缓存,使用如Class.forName("com.mysql.jdbc.Driver")加载jdbc驱动时会将Driver的Class对象缓存到
jdk对象的DriverManager中缓存了每个jdbc驱动的Class实例,而每个实例会存在一个对类加载器的引用(getClassLoader方法的返回值WebappClassLoader),这样会导致应用类加载器无法回收,此时 tomcat会强行从DriverManager的缓存中移除缓存的Class对象。
b. 停止应用创建的线程,因为每个线程创建后,此线程的线程上下文类加载器会继承父线程的上下文类加载器(这里是WebappClassLoader),我们需要手动解引用或停止线程:
1. 设置HttpClient的Keep-Alive-Timer线程的上下文类加载器为shareLoader,如果使用了jdk的HttpClient可能能会创建一个线程,且线程是上下文类加载器是WebappClassLoader
2. 停止Timer创建的定时线程(默认不停止,由开关clearReferencesStopTimerThreads指定)
3. 停止应用创建的线程,由clearReferencesStopThreads设置,默认false,
如果设为true,会检查线程是否由ThreadPoolExecutor创建,如果是,则调用ThreadPoolExecutor.shutdownNow停止线程,否则调用stop(废弃的不安全的)方法停止线程
c. 清除java.io.ObjectStreamClass$Caches缓存,由clearReferencesObjectStreamClassCaches指定,默认true。
ObjectStreamClass用于序列化和反序列化,当序列化一个java对象时,会将要序列化的对象的Class以ObjectStreamClass的形式缓存起来。
d. 清除Thread的属性threadLocals,inheritableThreadLocals中无用的entry,并检查有效entry的key/value是否由webappLoader加载
e. 移除rmi中通过webappLoader创建的存根。sun.rmi.transport.ObjectTable类有个静态属性objTable,所存储的Target类型对象的ccl(contextLoader)可能是webappLoader
f. 清除org.apache.tomcat.util.IntrospectionUtils的静态属性objectMethods的值(缓存)
g.如果使用了java.util.logging,需要reset Logger对象,因为静态属性final Handler emptyHandlers[]可能包含WebappClassLoader创建的类
h. 清除Introspector的静态属性declaredMethodCache
i. 移除注册的URLStreamHandlerFactory(由webappLoader创建的)
j. 置空对WebappClassLoader的引用
12. 调用StandardRoot的stop方法释放资源
13. 重置StandardContext(如置空监听器类实例的引用等,已创建的servlet实例等)
14. 置空当前Standard的实例化器InstanceManager(对象创建工厂)
16. 触发ThreadLocalLeakPreventionListener监听器的after_stop事件中断停止所有空闲线程,解决ThreadLocal引起的内存泄漏问题