Tomcat源码走读-内存泄露检测

来源:互联网 发布:怎么增加淘宝访客量 编辑:程序博客网 时间:2024/06/06 12:41

Tomcat7提供了对内存泄露的检测,其实现放在Web的类加载器WebappClassLoader中进行。在加载器停止时(也即stop函数中)执行内存泄露的检测。

  1. /**
  2. * 清除引用,防止内存泄露
  3. */
  4. protected void clearReferences() {
  5.  
  6. /**
  7. * 销毁注册的JDBC驱动
  8. */
  9. clearReferencesJdbc();
  10.  
  11. /**
  12. * 终止启动的线程
  13. */
  14. clearReferencesThreads();
  15.  
  16. /**
  17. * 检查ThreadLocal是否存在内存泄露
  18. */
  19. checkThreadLocalsForLeaks();
  20.  
  21. // 清除RMI对象
  22. clearReferencesRmiTargets();
  23.  
  24. if (clearReferencesStatic ) {
  25. clearReferencesStaticFinal();
  26. }
  27.  
  28. // 清除IntrospectionUtils的缓存,由于使用反射会对效率有一定的影响,因此做了相应的缓存
  29. IntrospectionUtils. clear();
  30.  
  31. ……
  32. ……
  33. }

 

首先看如何销毁注册的JDBC驱动,如:

  1. private final void clearReferencesJdbc() {
  2. /**
  3. * 加载JdbcLeakPrevention类
  4. */
  5. InputStream is = getResourceAsStream(
  6. "org/apache/catalina/loader/JdbcLeakPrevention.class" );
  7.  
  8. byte[] classBytes = new byte[2048];
  9. int offset = 0;
  10. try {
  11. int read = is.read(classBytes, offset, classBytes.length-offset);
  12. while (read > -1) {
  13. offset += read;
  14. if (offset == classBytes.length ) {
  15. // Buffer full - double size
  16. byte[] tmp = new byte[classBytes. length * 2];
  17. System. arraycopy(classBytes, 0, tmp, 0, classBytes.length);
  18. classBytes = tmp;
  19. }
  20. read = is.read(classBytes, offset, classBytes.length-offset);
  21. }
  22. Class<?> lpClass =
  23. defineClass("org.apache.catalina.loader.JdbcLeakPrevention" ,
  24. classBytes, 0, offset, this.getClass().getProtectionDomain());
  25. Object obj = lpClass.newInstance();
  26.  
  27. //通过反射调用clearJdbcDriverRegistrations方法以销毁注册的JDBC驱动
  28. List driverNames = (List) obj.getClass().getMethod(
  29. "clearJdbcDriverRegistrations" ).invoke(obj);
  30. for (String name : driverNames) {
  31. log.error(sm.getString("webappClassLoader.clearJdbc",
  32. getContextName(), name));
  33. }
  34. } catch (Exception e) {
  35. ……
  36. } finally {
  37. ……
  38. }
  39. }

 

这个函数的实现就是首先实例化JdbcLeakPrevention类,之后通过反射调用其clearJdbcDriverRegistrations方法来销毁注册的JDBC驱动。也许你会奇怪为什么这里不直接new一个JdbcLeakPrevention对象,再直接调用其方法,而要先读取class文件,之后通过defineClass来实例化类这么麻烦。这主要跟Java的类加载体系有关,首先看JdbcLeakPrevention是通过什么方式来检测未销毁的JDBC驱动的:

  1. public List clearJdbcDriverRegistrations() throws SQLException {
  2. List driverNames = new ArrayList<>();
  3.  
  4. /**
  5. * 获取当前加载的JDBC驱动列表
  6. */
  7. HashSet originalDrivers = new HashSet<>();
  8. Enumeration drivers = DriverManager.getDrivers();
  9. while (drivers.hasMoreElements()) {
  10. originalDrivers.add(drivers.nextElement());
  11. }
  12. /**
  13. * 再次加载JDBC列表,并以第二次为准
  14. */
  15. drivers = DriverManager.getDrivers();
  16. while (drivers.hasMoreElements()) {
  17. Driver driver = drivers.nextElement();
  18.  
  19. if (driver.getClass().getClassLoader() !=
  20. this.getClass().getClassLoader()) {
  21. continue;
  22. }
  23. //将未销毁的驱动名称返回
  24. if (originalDrivers.contains(driver)) {
  25. driverNames.add(driver.getClass().getCanonicalName());
  26. }
  27. DriverManager.deregisterDriver(driver);
  28. }
  29. return driverNames;
  30. }

 

可以看到这个函数是通过DriverManager的getDrivers来获取当前注册的驱动列表的,但是它只能获取跟当前的类加载器(也即WebappClassLoader)关联的驱动列表,由于这个类所在的jar包位于tomcat的lib目录下,因此如果这里不通过defineClass去加载它,则DriverManager将无法正常工作。

 

对于线程的处理,如:

  1. private void clearReferencesThreads() {
  2. //获取所在线程组的所有线程
  3. Thread[] threads = getThreads();
  4. List executorThreadsToStop = new ArrayList<>();
  5.  
  6. // 遍历
  7. for (Thread thread : threads) {
  8. if (thread != null) {
  9. ClassLoader ccl = thread.getContextClassLoader();
  10. //如果线程的上下文类加载器不是WebappClassLoader则不处理
  11. if (ccl == this) {
  12. if (thread == Thread.currentThread()) {
  13. continue ;
  14. }
  15.  
  16. // 如果是JVM的系统线程同样不处理
  17. ThreadGroup tg = thread.getThreadGroup();
  18. if (tg != null &&
  19. JVM_THREAD_GROUP_NAMES.contains(tg.getName())) {
  20.  
  21. ……//略
  22.  
  23. continue ;
  24. }
  25.  
  26. // 如果线程已经关闭则跳过
  27. if (!thread.isAlive()) {
  28. continue ;
  29. }
  30.  
  31. /**
  32. * 如果需要,对Timer线程做特殊处理
  33. */
  34. if (thread.getClass().getName().startsWith("java.util.Timer" ) &&
  35. clearReferencesStopTimerThreads) {
  36. clearReferencesStopTimerThread(thread);
  37. continue ;
  38. }
  39.  
  40. if (isRequestThread(thread)) {
  41. log.error(sm.getString("webappClassLoader.warnRequestThread" ,
  42. getContextName(), thread.getName()));
  43. } else {
  44. log.error(sm.getString("webappClassLoader.warnThread" ,
  45. getContextName(), thread.getName()));
  46. }
  47.  
  48. /**
  49. * 判断 Tomcat是否应该处理web启动的线程,false则直接返回
  50. */
  51. if (!clearReferencesStopThreads) {
  52. continue ;
  53. }
  54.  
  55. /**
  56. * 判断线程是否是通过Executor启动的,需做不同的处理
  57. */
  58. boolean usingExecutor = false;
  59. try {
  60. Object target = null ;
  61. for (String fieldName : new String[] { "target",
  62. "runnable" , "action" }) {
  63. try {
  64. Field targetField = thread.getClass()
  65. .getDeclaredField(fieldName);
  66. targetField.setAccessible( true );
  67. target = targetField.get(thread);
  68. break ;
  69. } catch (NoSuchFieldException nfe) {
  70. continue ;
  71. }
  72. }
  73.  
  74. if (target != null &&
  75. target.getClass().getCanonicalName() != null
  76. && target.getClass().getCanonicalName().equals(
  77. "java.util.concurrent.ThreadPoolExecutor.Worker" )) {
  78. Field executorField =
  79. target.getClass().getDeclaredField("this$0" );
  80. executorField.setAccessible( true );
  81. Object executor = executorField.get(target);
  82. if (executor instanceof ThreadPoolExecutor) {
  83. ((ThreadPoolExecutor) executor).shutdownNow();
  84. usingExecutor = true ;
  85. }
  86. }
  87. }
  88. ……//略
  89.  
  90. //如果是通过Executor启动,先添加到链表,否则直接停止
  91. if (usingExecutor) {
  92. executorThreadsToStop.add(thread);
  93. } else {
  94. thread.stop();
  95. }
  96. }
  97. }
  98. }
  99.  
  100. /**
  101. * 逐一终止通过Executor执行的线程,不过每个线程都给予一定的时间进行关闭
  102. */
  103. int count = 0;
  104. for (Thread t : executorThreadsToStop) {
  105. while (t.isAlive() && count < 100) {
  106. try {
  107. Thread.sleep(20);
  108. } catch (InterruptedException e) {
  109. // Quit the while loop
  110. break ;
  111. }
  112. count++;
  113. }
  114. if (t.isAlive()) {
  115. t.stop();
  116. }
  117. }
  118. }

 

逻辑很长,归结起来就是遍历当前线程所在线程组的所有线程,如果用户配置了可以终止线程,则逐一处理,在这个过程中,需要不同类型的线程进行区别对待,比如说JVM的系统线程不要处理,Timer线程特殊处理,而通过Executor启动的线程和非通过Executor启动的又需采用不同的方法终止,等等。

 

最后一个需要提到的是由于没有释放ThreadLocal引用对象导致的内存泄露,这里的checkThreadLocalLeaks只会对可能的内存泄露进行检测并进行警告,不会直接进行处理。检测的方法其实跟上面对线程的处理方法差不多,遍历线程,对线程持有的ThreadLocal对象进行检测。


本文转载自:http://www.jmatrix.org/java/j2ee/831.html

原创粉丝点击