优秀的编程知识分享平台

网站首页 > 技术文章 正文

JVM调优——Java动态编译过程中的内存溢出问题

nanyue 2024-09-21 20:06:45 技术文章 9 ℃

由于测试环境项目每2小时内存就溢出一次, 分析问题,发现Java动态加载Class并运行那块存在内存溢出问题, 遂本地调测。

一、找到动态编译那块的代码,具体如下


  1. /**
  2. * @MethodName: 编译java代码到Object
  3. * @Description
  4. * @param fullClassName 类名
  5. * @param javaCode 类代码
  6. * @return Object
  7. * @throws IllegalAccessException
  8. * @throws InstantiationException
  9. */
  10. public Class javaCodeToObject(String fullClassName, String javaCode) throws IllegalAccessException, InstantiationException {
  11. Object instance = null;
  12. //获取系统编译器
  13. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  14. // 建立DiagnosticCollector对象
  15. DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
  16. // 建立用于保存被编译文件名的对象
  17. // 每个文件被保存在一个从JavaFileObject继承的类中
  18. ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));
  19. List<JavaFileObject> jfiles = new ArrayList<>();
  20. jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));
  21. //使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
  22. List<String> options = new ArrayList<>();
  23. options.add("-encoding");
  24. options.add("UTF-8");
  25. options.add("-classpath");
  26. options.add(this.classpath);
  27. //不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
  28. options.add("-XDuseUnsharedTable");
  29. JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
  30. // 编译源程序
  31. boolean success = task.call();
  32. if (success) {
  33. //如果编译成功,用类加载器加载该类
  34. JavaClassObject jco = fileManager.getJavaClassObject();
  35. DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);
  36. Class clazz = dynamicClassLoader.loadClass(fullClassName,jco);
  37. try {
  38. dynamicClassLoader.close();
  39. //卸载ClassLoader所加载的类
  40. ClassLoaderUtil.releaseLoader(dynamicClassLoader);
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. return clazz;
  45. } else {
  46. //如果想得到具体的编译错误,可以对Diagnostics进行扫描
  47. String error = "";
  48. for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
  49. error = error + compilePrint(diagnostic);
  50. }
  51. }
  52. return null;
  53. }

二、本地写测试类,并且启动执行

本地动态加载1000个类,测试查看内存空间变化


  1. public static void main(String[] args) {
  2. String code = "import java.util.HashMap;\n" +
  3. "import com.yunerp.web.vaadin.message.alert;\n" +
  4. "import java.util.List;\n" +
  5. "import java.util.ArrayList;\n" +
  6. "import com.yunerp.web.vaadin.util.modularfuntion.base.BaseUtil;\n" +
  7. "import com.yunerp.web.vaadin.util.function.TableFuntionUtil;\n" +
  8. "import com.yunerp.web.vaadin.util.modularfuntion.stoUtil.StoUtil;\n" +
  9. "import java.util.Map;import com.yunerp.web.vaadin.util.modularfuntion.user.mini.HomePageUtil;\n" +
  10. "import com.yunerp.web.util.run.WebInterface;\n" +
  11. "\n" +
  12. "public class web2905763164651825363 implements WebInterface {\n" +
  13. " public Object execute(Map<String,Object> param) {\n" +
  14. " System.out.println(param.get(\"key\"));" +
  15. " return null;\n" +
  16. " }\n" +
  17. "}";
  18. String name = "web2905763164651825363";
  19. for(int i=0;i<1000;i++){
  20. long time1 = System.currentTimeMillis();
  21. DynamicEngine de = DynamicEngine.getInstance();
  22. try {
  23. Class cl = de.javaCodeToObject(name,code);
  24. WebInterface webInterface = (WebInterface)cl.newInstance();
  25. Map<String,Object> param = new HashMap<>();
  26. param.put("key",i);
  27. webInterface.execute(param);
  28. }catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. System.gc();
  32. long time2 = System.currentTimeMillis();
  33. System.out.println("次数:"+i+" time:"+(time2-time1));
  34. }
  35. }

三、使用JConsole和JVisualVM工具进行检测。

工具的使用方法:JConsole和JVisualVM工具使用

本地项目启动后,使用JConsole和 JVisualVM工具进行检测,发现在动态加载类时, 堆空间内存直线上升,但是所加载的类和实例都被释放了,而且ClassLoader也释放了,但是内存还是在 上升,发现结果如下:

在查看堆空间快照的时候,发现JDK自带的 com.sun.tools.javac.util.SharedNameTable.NameImpl 类及其实例所在的内存空间比达到52%。 具体如下:

四、分析问题

查了很多文献,也问了很多朋友,都对SharedNameTable这个类很陌生,最终还是在google上找到我想要的解答。具体如下两个链接

链接:https://stackoverflow.com/questions/14617340/memory-leak-when-using-jdk-compiler-at-runtime

大概意思是:

Java 7引入了这个错误:为了加速编译,他们引入了SharedNameTable,它使用软引用来避免重新分配,但不幸的是只会导致JVM膨胀失控,因为这些软引用永远不会被回收直到JVM达到-Xmx内存限制。据称它将在Java 9中修复。与此同时,还有一个(未记录的)编译器选项来禁用它:-XDuseUnsharedTable。

参考链接2:https://stackoverflow.com/questions/33548218/memory-leak-in-program-using-compiler-api

五、 内存溢出问题解决

在编译选项options中加入 "-XDuseUnsharedTable" ,重新编译运行,内存溢出问题解决


  1. //使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
  2. List<String> options = new ArrayList<>();
  3. options.add("-encoding");
  4. options.add("UTF-8");
  5. options.add("-classpath");
  6. options.add(this.classpath);
  7. //不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
  8. options.add("-XDuseUnsharedTable");

重新运行的效果图如下:

至此,问题完美解决。

最近发表
标签列表