【Big Data 每日一题20180822】Java动态编译优化——URLClassLoader 内存泄漏问题解决

转 https://blog.csdn.net/shijing266/article/details/81939477

一、动态编译案例

要说动态编译内存泄漏,首先我们先看一个案例(网上搜动态编译的资料是千篇一律,只管实现功能,不管内存泄漏,并且都恬不知耻的标识为原创!!)

Java  URLClassLoader 动态编译案例:https://blog.csdn.net/huangshanchun/article/details/72835647

这篇文章和我google搜的其他文章、资料一样,属于JDK1.6以后的版本。确实能实现动态编译并加载,但是却存在严重的URLClassLoader内存泄漏的问题,并且存在SharedNameTable 和  ZipFileIndex的内存泄漏问题。

其中SharedNameTable问题我已经解决:参考

二、URLClassLoader问题分析和解决

1、问题发现

生产环境JVM的运行情况,OLD区爆满,FULlGC不停的执行,项目大概2小时挂掉了,如下图:

【Big Data 每日一题20180822】Java动态编译优化——URLClassLoader 内存泄漏问题解决_第1张图片

 

在使用VisualVM和 JProfile 两者工具远程分析 测试环境和生产环境的项目后,转储堆Dump文件,并转存到本地分析。 发现动态编译这块存在URLClassLoader的内存泄漏,如下图所示:

【Big Data 每日一题20180822】Java动态编译优化——URLClassLoader 内存泄漏问题解决_第2张图片

 

【Big Data 每日一题20180822】Java动态编译优化——URLClassLoader 内存泄漏问题解决_第3张图片

 

2、问题分析

URLClassLoader占了83%的内存空间,遂研究了一下动态编译这块的代码,原案例代码如下:

 
  1. import javax.tools.*;

  2. import java.io.File;

  3. import java.net.URL;

  4. import java.net.URLClassLoader;

  5. import java.util.ArrayList;

  6. import java.util.List;

  7.  
  8. public class DynamicCompile {

  9. private URLClassLoader parentClassLoader;

  10. private String classpath;

  11. public DynamicCompile() {

  12. this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();

  13. this.buildClassPath();// 存在动态安装的问题,需要动态编译类路径

  14. }

  15.  
  16. private void buildClassPath() {

  17. this.classpath = null;

  18. StringBuilder sb = new StringBuilder();

  19. for (URL url : this.parentClassLoader.getURLs()) {

  20. String p = url.getFile();

  21. sb.append(p).append(File.pathSeparator); //路径分割符linux为:window系统为;

  22. }

  23. this.classpath = sb.toString();

  24. }

  25. /**

  26. * 编译出类

  27. *

  28. * @param fullClassName 全路径的类名

  29. * @param javaCode java代码

  30. *

  31. * @return 目标类

  32. */

  33. public Class compileToClass(String fullClassName, String javaCode) throws Exception {

  34. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

  35. DiagnosticCollector diagnostics = new DiagnosticCollector<>();

  36. ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

  37.  
  38. List jfiles = new ArrayList<>();

  39. jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));

  40.  
  41. List options = new ArrayList<>();

  42. options.add("-encoding");

  43. options.add("UTF-8");

  44. options.add("-classpath");

  45. options.add(this.classpath);

  46.  
  47. JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);

  48. boolean success = task.call();

  49.  
  50. if (success) {

  51. JavaClassObject jco = fileManager.getJavaClassObject();

  52. DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);

  53. //加载至内存

  54. return dynamicClassLoader.loadClass(fullClassName, jco);

  55. } else {

  56. for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {

  57. String error = compileError(diagnostic);

  58. throw new RuntimeException(error);

  59. }

  60. throw new RuntimeException("compile error");

  61. }

  62. }

  63.  
  64. private String compileError(Diagnostic diagnostic) {

  65. StringBuilder res = new StringBuilder();

  66. res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");

  67. res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");

  68. res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");

  69. return res.toString();

  70. }

  71. }

URLClassLoader这里使用的是全局变量,并且是获取的当前类的ClassLoader(总的) ,在最后加载完class后,并没有关闭操作

this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();

我想,那么用完之后我给这个parentClassLoader进行close不就解决了?  我想的太简单了。

切忌:此处的URLClassLoader不能关闭,因为用的是当前所在类的ClassLoader,如果你关闭了,那么会导致你当前程序的其他类会ClassNotFoundException

 

3、问题解决(三种)。

1、因为这里使用的是源代码的内存级动态编译,即:

new CharSequenceJavaFileObject(fullClassName, javaCode)

所以,可以用自定义的FileManager 去获取classLoader  ,参考:https://www.cnblogs.com/whuqin/p/4981948.html

但是这里因为是用的ClassLoader而不是URLClassLoader,其实也没法进行close。具体我没去测试有没有内存泄漏。

2、也可以使用源代码的文件级动态编译,去获取文件对应的URLClassLoader。

3、既然不能关闭全局的ClassLoader,又想用URLClassLoader,看了官网URLClassLoader的API后,想到其实可以自己new 一个URLClassLoader来处理动态编译后的Class加载。 毕竟自己new出来的可以直接关闭,不会影响全局类的加载,具体如下:

 
  1. package com.yunerp.web.util.run.compile;

  2.  
  3. import org.apache.log4j.Logger;

  4. import sun.misc.ClassLoaderUtil;

  5.  
  6. import javax.tools.DiagnosticCollector;

  7. import javax.tools.JavaCompiler;

  8. import javax.tools.JavaFileObject;

  9. import javax.tools.ToolProvider;

  10. import java.io.File;

  11.  
  12. import java.net.URL;

  13. import java.net.URLClassLoader;

  14. import java.util.ArrayList;

  15. import java.util.List;

  16.  
  17.  
  18. public class DynamicEngine {

  19.  
  20. private final Logger log = Logger.getLogger(this.getClass().getName());

  21.  
  22.  
  23. /**

  24. * @MethodName : 创建classpath

  25. * @Description

  26. */

  27. private String buildClassPath() {

  28. StringBuilder sb = new StringBuilder();

  29. URLClassLoader parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();

  30. for (URL url : parentClassLoader.getURLs()) {

  31. String p = url.getFile();

  32. sb.append(p).append(File.pathSeparator);

  33. }

  34. return sb.toString();

  35. }

  36.  
  37. /**

  38. * @param fullClassName 类名

  39. * @param javaCode 类代码

  40. * @return Object

  41. * @throws IllegalAccessException

  42. * @throws InstantiationException

  43. * @MethodName : 编译java代码到Object

  44. * @Description

  45. */

  46. public Class javaCodeToObject(String fullClassName, final String javaCode) throws IllegalAccessException, InstantiationException {

  47.  
  48. DynamicClassLoader dynamicClassLoader = null;

  49. ClassFileManager fileManager = null;

  50. List jfiles = null;

  51. JavaClassObject jco = null;

  52. URLClassLoader urlClassLoader = null;

  53. try {

  54. //获取系统编译器

  55. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

  56.  
  57. // 建立DiagnosticCollector对象

  58. DiagnosticCollector diagnostics = new DiagnosticCollector<>();

  59. //设置系统属性

  60. System.setProperty("useJavaUtilZip", "true");

  61. // 建立用于保存被编译文件名的对象

  62. // 每个文件被保存在一个从JavaFileObject继承的类中

  63. fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

  64.  
  65. jfiles = new ArrayList<>();

  66. jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));

  67.  
  68. //使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合

  69. List options = new ArrayList<>();

  70. options.add("-encoding");

  71. options.add("UTF-8");

  72. options.add("-classpath");

  73. //获取系统构建路径

  74. options.add(buildClassPath());

  75. //不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)

  76. options.add("-XDuseUnsharedTable");

  77. //设定使用javaUtilZip,避免zipFileIndex泄漏

  78. options.add("-XDuseJavaUtilZip");

  79.  
  80. JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);

  81.  
  82. // 编译源程序

  83. boolean success = task.call();

  84.  
  85. if (success) {

  86. //如果编译成功,用类加载器加载该类

  87. jco = fileManager.getJavaClassObject();

  88. URL[] urls = new URL[]{new File("").toURI().toURL()};

  89. //获取类加载器(每一个文件一个类加载器)

  90. urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());

  91. dynamicClassLoader = new DynamicClassLoader(urlClassLoader);

  92. Class clazz = dynamicClassLoader.loadClass(fullClassName, jco);

  93. return clazz;

  94. } else {

  95. log.error("编译失败: "+ fullClassName);

  96. }

  97. } catch (Exception e) {

  98. e.printStackTrace();

  99. } finally {

  100. try {

  101. //卸载ClassLoader所加载的类

  102. if (dynamicClassLoader != null) {

  103. dynamicClassLoader.close();

  104. ClassLoaderUtil.releaseLoader(dynamicClassLoader);

  105. }

  106. if (urlClassLoader != null) {

  107. urlClassLoader.close();

  108. }

  109. if (fileManager != null) {

  110. fileManager.flush();

  111. fileManager.close();

  112. }

  113. if (jco != null) {

  114. jco.close();

  115. }

  116. jfiles = null;

  117. } catch (Exception e) {

  118. e.printStackTrace();

  119. }

  120. }

  121. return null;

  122. }

  123. }

  124.  
  125.  
  126.  

重新发布后,测试1天的结果如下:

【Big Data 每日一题20180822】Java动态编译优化——URLClassLoader 内存泄漏问题解决_第4张图片

至此:URLClassLoader问题解决,JVM的 OLD区正常,项目能正常运行一周左右(之前是2-4小时就内存泄漏挂掉了)

 

补充说明:

1、我这里使用URLClassLoader是new的一个空文件流,为什么选择这么做,因客观原因,必须要用源代码的内存级动态编译,这样我无法获取到文件的具体全路径。

2、其实可以优化的更彻底,即我去除options参数里面的classpath,这样就能不用全局的ClassLoader了,  一般来说,只要配置了环境变量CLASSPATH,项目运行就能获取到,但是不知道是否是服务器环境问题,开发和测试环境Linux没法取到classpath,导致编译失败。所以这里我还是保留了buildClassPath()方法。但是总体效果还是很明显了,虽然我有点强迫症。只能等后续有时间了再去研究了。

3、另外,代码中我加上了关于useJavaUtilZip的配置,以为能解决ZipFileIndex的问题,但是实际上这个问题仍然存在,但是影响不是那么大,等待后续或者其他人来研究了。

你可能感兴趣的:(Big,Data,每日一题,java及相关源码分析)