Tomcat7提供了对内存泄露的检测,其实现放在Web的类加载器WebappClassLoader中进行。在加载器停止时(也即stop函数中)执行内存泄露的检测。
- /**
- * 清除引用,防止内存泄露
- */
- protected void clearReferences() {
- /**
- * 销毁注册的JDBC驱动
- */
- clearReferencesJdbc();
- /**
- * 终止启动的线程
- */
- clearReferencesThreads();
- /**
- * 检查ThreadLocal是否存在内存泄露
- */
- checkThreadLocalsForLeaks();
- // 清除RMI对象
- clearReferencesRmiTargets();
- if (clearReferencesStatic ) {
- clearReferencesStaticFinal();
- }
- // 清除IntrospectionUtils的缓存,由于使用反射会对效率有一定的影响,因此做了相应的缓存
- IntrospectionUtils. clear();
- ……
- ……
- }
首先看如何销毁注册的JDBC驱动,如:
- private final void clearReferencesJdbc() {
- /**
- * 加载JdbcLeakPrevention类
- */
- InputStream is = getResourceAsStream(
- "org/apache/catalina/loader/JdbcLeakPrevention.class" );
- byte[] classBytes = new byte[2048];
- int offset = 0;
- try {
- int read = is.read(classBytes, offset, classBytes.length-offset);
- while (read > -1) {
- offset += read;
- if (offset == classBytes.length ) {
- // Buffer full - double size
- byte[] tmp = new byte[classBytes. length * 2];
- System. arraycopy(classBytes, 0, tmp, 0, classBytes.length);
- classBytes = tmp;
- }
- read = is.read(classBytes, offset, classBytes.length-offset);
- }
- Class<?> lpClass =
- defineClass("org.apache.catalina.loader.JdbcLeakPrevention" ,
- classBytes, 0, offset, this.getClass().getProtectionDomain());
- Object obj = lpClass.newInstance();
- //通过反射调用clearJdbcDriverRegistrations方法以销毁注册的JDBC驱动
- List driverNames = (List) obj.getClass().getMethod(
- "clearJdbcDriverRegistrations" ).invoke(obj);
- for (String name : driverNames) {
- log.error(sm.getString("webappClassLoader.clearJdbc",
- getContextName(), name));
- }
- } catch (Exception e) {
- ……
- } finally {
- ……
- }
- }
这个函数的实现就是首先实例化JdbcLeakPrevention类,之后通过反射调用其clearJdbcDriverRegistrations方法来销毁注册的JDBC驱动。也许你会奇怪为什么这里不直接new一个JdbcLeakPrevention对象,再直接调用其方法,而要先读取class文件,之后通过defineClass来实例化类这么麻烦。这主要跟Java的类加载体系有关,首先看JdbcLeakPrevention是通过什么方式来检测未销毁的JDBC驱动的:
- public List clearJdbcDriverRegistrations() throws SQLException {
- List driverNames = new ArrayList<>();
- /**
- * 获取当前加载的JDBC驱动列表
- */
- HashSet originalDrivers = new HashSet<>();
- Enumeration drivers = DriverManager.getDrivers();
- while (drivers.hasMoreElements()) {
- originalDrivers.add(drivers.nextElement());
- }
- /**
- * 再次加载JDBC列表,并以第二次为准
- */
- drivers = DriverManager.getDrivers();
- while (drivers.hasMoreElements()) {
- Driver driver = drivers.nextElement();
- if (driver.getClass().getClassLoader() !=
- this.getClass().getClassLoader()) {
- continue;
- }
- //将未销毁的驱动名称返回
- if (originalDrivers.contains(driver)) {
- driverNames.add(driver.getClass().getCanonicalName());
- }
- DriverManager.deregisterDriver(driver);
- }
- return driverNames;
- }
可以看到这个函数是通过DriverManager的getDrivers来获取当前注册的驱动列表的,但是它只能获取跟当前的类加载器(也即WebappClassLoader)关联的驱动列表,由于这个类所在的jar包位于tomcat的lib目录下,因此如果这里不通过defineClass去加载它,则DriverManager将无法正常工作。
对于线程的处理,如:
- private void clearReferencesThreads() {
- //获取所在线程组的所有线程
- Thread[] threads = getThreads();
- List executorThreadsToStop = new ArrayList<>();
- // 遍历
- for (Thread thread : threads) {
- if (thread != null) {
- ClassLoader ccl = thread.getContextClassLoader();
- //如果线程的上下文类加载器不是WebappClassLoader则不处理
- if (ccl == this) {
- if (thread == Thread.currentThread()) {
- continue ;
- }
- // 如果是JVM的系统线程同样不处理
- ThreadGroup tg = thread.getThreadGroup();
- if (tg != null &&
- JVM_THREAD_GROUP_NAMES.contains(tg.getName())) {
- ……//略
- continue ;
- }
- // 如果线程已经关闭则跳过
- if (!thread.isAlive()) {
- continue ;
- }
- /**
- * 如果需要,对Timer线程做特殊处理
- */
- if (thread.getClass().getName().startsWith("java.util.Timer" ) &&
- clearReferencesStopTimerThreads) {
- clearReferencesStopTimerThread(thread);
- continue ;
- }
- if (isRequestThread(thread)) {
- log.error(sm.getString("webappClassLoader.warnRequestThread" ,
- getContextName(), thread.getName()));
- } else {
- log.error(sm.getString("webappClassLoader.warnThread" ,
- getContextName(), thread.getName()));
- }
- /**
- * 判断 Tomcat是否应该处理web启动的线程,false则直接返回
- */
- if (!clearReferencesStopThreads) {
- continue ;
- }
- /**
- * 判断线程是否是通过Executor启动的,需做不同的处理
- */
- boolean usingExecutor = false;
- try {
- Object target = null ;
- for (String fieldName : new String[] { "target",
- "runnable" , "action" }) {
- try {
- Field targetField = thread.getClass()
- .getDeclaredField(fieldName);
- targetField.setAccessible( true );
- target = targetField.get(thread);
- break ;
- } catch (NoSuchFieldException nfe) {
- continue ;
- }
- }
- if (target != null &&
- target.getClass().getCanonicalName() != null
- && target.getClass().getCanonicalName().equals(
- "java.util.concurrent.ThreadPoolExecutor.Worker" )) {
- Field executorField =
- target.getClass().getDeclaredField("this$0" );
- executorField.setAccessible( true );
- Object executor = executorField.get(target);
- if (executor instanceof ThreadPoolExecutor) {
- ((ThreadPoolExecutor) executor).shutdownNow();
- usingExecutor = true ;
- }
- }
- }
- ……//略
- //如果是通过Executor启动,先添加到链表,否则直接停止
- if (usingExecutor) {
- executorThreadsToStop.add(thread);
- } else {
- thread.stop();
- }
- }
- }
- }
- /**
- * 逐一终止通过Executor执行的线程,不过每个线程都给予一定的时间进行关闭
- */
- int count = 0;
- for (Thread t : executorThreadsToStop) {
- while (t.isAlive() && count < 100) {
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- // Quit the while loop
- break ;
- }
- count++;
- }
- if (t.isAlive()) {
- t.stop();
- }
- }
- }
逻辑很长,归结起来就是遍历当前线程所在线程组的所有线程,如果用户配置了可以终止线程,则逐一处理,在这个过程中,需要不同类型的线程进行区别对待,比如说JVM的系统线程不要处理,Timer线程特殊处理,而通过Executor启动的线程和非通过Executor启动的又需采用不同的方法终止,等等。
最后一个需要提到的是由于没有释放ThreadLocal引用对象导致的内存泄露,这里的checkThreadLocalLeaks只会对可能的内存泄露进行检测并进行警告,不会直接进行处理。检测的方法其实跟上面对线程的处理方法差不多,遍历线程,对线程持有的ThreadLocal对象进行检测。