本文主要讲述一下应用卸载的时候,WebAppClassLoader的clearReferences 方法:

该方法是应用Context stop进行lifecycle stop的时候,同时关闭Loader组件,我们知道Loader组件关联着WebappClassLoader的组件,上述的代码就是WebappClassLoader在进行关闭和清理工作需要进行的项目。
下面,我们一个一个项目来进行分析和研究。
1.clearReferenceJdbc
JDBC的Driver资源是非常宝贵的,对于应用当中,经常存在一些不规范的写法,而其中Driver的关闭尤其严重:
应用是卸载掉了,但是应用中的关联数据库的驱动却没有卸载干净,这会造成什么样的问题呢?
如下图所示:

如果应用1应用代码写的比较垃圾,应用是卸载掉了,那就会造成当前的JVM中的DirverManager中的Driver清理不干净;
来看看Tomcat是怎么做的:

实际就是DriverManager能拿到所有的Driver的一个集合,然后判断Driver该类是否是由当前应用类加载器进行加载的,如果是的话,直接调用DriverManager.deregisterDriver对其进行卸载;
2.clearRerencesThread
clearRerencesThread这部分在 StandardContext的属性之四 中已经讲解过了,通过StandardContext的几个属性来控制是否clear掉当前应用创建出来的线程。
主要的思路是,首先通过当前的ThreadGroup来拿到当前Tomcat启动(也就是jvm虚拟机)的所有线程,拿到之后比对当前thread.contextClassLoader是否就是当前应用的webappClassLoader,如果一样,说明该thread就是当前应用创建出来的线程。
之后,Tomcat针对JVM的线程,Timer线程,JDK线程池ThreadExecutor中创建的线程等多种类型的线程,给出其清理的办法。
这里就不再缀余,有需要的读者可以回顾一下。
3.clearRerencesRmiTarget
同样在 StandardContext的属性之四 中也进行了讲述,也是StandardContext中开放了一个属性作为是否要清理RMI的target的开关。
对于RMI的Target调用的是SUN的实现类,也就是仅仅针对于sun jdk(oracle jdk)生效,对于其他的JDK不生效,调用方式也是通过反射拿sun.xxx的包进行清除。
4.clearRerencesStaticFinals
对于该应用的创建的JVM内存对象,随着GC的回收旧清空了,这个不用太操心。
但有一种,该清理主要针对于static和final类型的,主要目的为了GC能够更快回收static和final的类,如下图所示:

应用中有一个ClassB,在Class B中持有一个Class A的静态属性,以这种情况为例,GC在回收的时候Class B,如果Class B中持有的static A属性为null的话,Class B立刻被回收,而A对象也没有关联,也会立刻被回收;

如上述代码中,resourceEntries实际就是WebappClassLoader已经load的Class资源,WebappClassLoader已经将其缓存起来,在WebappClassLoader要结束其使命的时候,将这些已经load的Class逐个遍历出来,判断其field是否有final或者static的属性,将其设置为null;
这样Class B就很快就被回收掉了;
那么有同学提出问题,你WebappClassLoader最后都没了,那么Class B你还做这个干毛啊?
说的没错,这种做法和设置对象的属性为null一个效果,加快GC回收的命中率,让GC程序来的更高效一些,以免浪费多余的CPU。
5.InstrospectionUtils反射资源清理
Tomcat中有一个反射的Utils工具,挺好用的,Tomcat很多地方都是直接调用这个Utils进行反射的,而该类之所以好用的其中一点就是其有一个缓存:

如上述代码,ObjectMethods缓存的是Class和该Class已经反射的方法,如果随着应用越多,这个ObjectMethods也越多;
而对于某个应用就直接调用ObjectMethods缓存的clear全清空的做法,笔者对Tomcat这种做法有一些怀疑,笔者认为应该设计成不同ClassLoader于Class,Method[]的三级映射,你把该Web应用的WebappClassLoader中的缓存都干掉不就完事了吗?
不过也许搞这块的Tomcat committer不这么想,卸掉一个应用我就重置一次...,不纠结了吧,反正至少这块是Tomcat是清了,比不清要好。
对于InstroSpector在JDK中也有实现,对应类是java.bean.InstroSpectors,这个类是干啥的呢?
它其实也是反射,在JDK通过这样的一套API用以访问对象的get,set方法的,换而言之就是内省机制,你们知道BeanUtils吧,这个东西其实就是InstroSpectors这些类搞的东西;
而调用其flushCaches,实际也就是和上面Tomcat的那个InstroSpectionsUtils一样的道理,也是拿到当前线程的反射信息,都给干掉即可;

6.Logger和ClassLoader脱钩
关于Logger实际上是和ClassLoader有关联的,从下面的代码中,我们可以得知,其之所以让ClassLoader于Logger脱钩,我们是为了拿到当前WebappClassLoader的在Logger注册的CLassLoaderInfo信息,而获得该信息的最终目的是让CLassLoaderInfo信息中的Handler从Logger中清除;

7.ResourceBundle解除
我们一般在应用中进行资源读取,一般都会用到ResourceBundle,但我们一定注意的是,当前应用的WebClassLoader再读取资源的时候,和ResourceBundle是有关联的,可以看看ResourceBundle的源码:

从源码结构上分析,一层套一层,反正和当前WebAppClassLoader是有关系的,那Tomcat的做法也很简单,就是将其干掉:

不过,我们发现其loaderRef是WeakReference,WeakReference当引用对象不存在立刻就回收的,而该引用对象:

实质就是WebAppClassLoader,那么GC直接立刻就回收的,那么得出的是做这个多余?
我们从注释中就可以推导出我前面研究的结论:

确实这个不清也可以,这种清空的意义其实和前面clearReferncesStaticFinals一样,都是加快GC的命中率,你不搞GC也能慢慢清掉,就是耗时耗力呗!
8.TomcatURLStreamHandlerFactory的清除
tomcat中用于URL协议工厂的创建也与ClassLoader相关联:

9.ThreadLocal的清除
最后聊聊ThreadLocal的事,ThreadLocal是当前Thread类中维护的两个缓存:

threadLocals是当前线程自己的线程缓存,而inheritableThreadLocals是当前线程创建的子线程继承下来线程缓存,如果要清理的话,这两个属性都需要清理;
那怎么清空呢?
实际上你需要调用ThreadLocal中维护ThreadLocalMap的expungStaleEntried方法就行了:

该方法主要是干掉ThreadLocalMap中的table[]数组中的Entry元素,这个Entry就是ThreadLocal种缓存的实际的xxx,而当xxx的关联被解除以后,这个ThreadLocalMap也会得以释放,从而让当前应用lifecycle的ThreadLocal得以最终的释放;
我们看看代码吧:

其主要按照我们分析的进行,针对于当前Thread的threadLocals和inheritableThreadLocals进行下手,做法就是调用其ThreadLocal对象的内部ThreadLocalMap类中的expungeStaleEntries方法,将ThreadLocalMap中的table置空;
上述代码需要注意有三点:
1.getThread()方法中,返回的是当前Thread的一个ThreadGroup,之所以采用了递归,因为ThreadGroup可以无限嵌套,需要用递归来找出最根的那个ThreadGroup;
找到之后使用threadLocals和inheritableThreadLocals .get(ThreadGroup)进行过滤;
2.最后为什么都已经expungStaleEntries方法,将table清空了,怎么还要再调用一个checkThreadLocalMapForLeaks进行泄露检测呢?
这是因为当你进行这项操作的时候,难免应用不会对ThreadLocal进行操作,你虽然已经清空,但这时可能table又加了新值,那么这时你需要再进行判断和校验一下,我们浏览一下这个方法:

该方法其实你看到啥都没干,就是记录记录log提醒提醒你而已;
3.我们更需要注意的一点是,该ThreadLocal是哪个线程的?

我们一定要注意,该threadLocal和http请求工作线程的上下文的ThreadLocal完全不是一个,该threadLocal是应用lifecycle的,随着应用的启动和生命周期事件启停的,我们清空的是在应用的创建过程中的ThreadLocal,而不是请求过程中的ThreadLocal的,那个随着请求线程的消失和归还ThreadLocal已经都都reset了;
总结:
本文讲了讲应用卸载的时候WbappClassLoader中的cleanRerences,总共分成9类,可以看到Tomcat其实真是有够辛苦的了,能清理干净的都清理干净,笔者再没有看这段代码之前,好多都没想到,也是学习了;
但试想一下,即使这么搞,Tomcat中还是否有没有清干净的呢?
我想还是有的,但是Tomcat能做到这个份上已经不错了;
而应用如果写的糟糕的话,Tomcat再咋搞也清理不了,例如应用中自己搞一个自定义ClassLoader,我们看到当前cleanRerences大多都是与ClassLoader进行匹配关联,才能从一些全局类中清空一些和当前应用挂钩的资源的,如果自定义搞,那么好多资源都清理不干净。。。类似这样的问题还有很多。。
所以,我们学习这段代码的同时,在写应用的时候真是要一定注意,别让Tomcat费这大力气清空一些东西,你应用中随便留了一大坨垃圾在里面,久而久之,啥应用服务器也会崩掉啊!




