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

由于测试环境项目每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.  
    // 建立用于保存被编译文件名的对象
  18.  
    // 每个文件被保存在一个从JavaFileObject继承的类中
  19.  
    ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));
  20.  
     
  21.  
    List<JavaFileObject> jfiles = new ArrayList<>();
  22.  
    jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));
  23.  
     
  24.  
    //使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
  25.  
    List<String> options = new ArrayList<>();
  26.  
    options.add("-encoding");
  27.  
    options.add("UTF-8");
  28.  
    options.add("-classpath");
  29.  
    options.add(this.classpath);
  30.  
    //不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
  31.  
    options.add("-XDuseUnsharedTable");
  32.  
     
  33.  
    JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
  34.  
     
  35.  
    // 编译源程序
  36.  
    boolean success = task.call();
  37.  
     
  38.  
    if (success) {
  39.  
    //如果编译成功,用类加载器加载该类
  40.  
    JavaClassObject jco = fileManager.getJavaClassObject();
  41.  
    DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);
  42.  
    Class clazz = dynamicClassLoader.loadClass(fullClassName,jco);
  43.  
    try {
  44.  
    dynamicClassLoader.close();
  45.  
    //卸载ClassLoader所加载的类
  46.  
    ClassLoaderUtil.releaseLoader(dynamicClassLoader);
  47.  
    } catch (IOException e) {
  48.  
    e.printStackTrace();
  49.  
    }
  50.  
    return clazz;
  51.  
    } else {
  52.  
    //如果想得到具体的编译错误,可以对Diagnostics进行扫描
  53.  
    String error = "";
  54.  
    for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
  55.  
    error = error + compilePrint(diagnostic);
  56.  
    }
  57.  
    }
  58.  
    return null;
  59.  
    }

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

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

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

三、使用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");

重新运行的效果图如下:

至此,问题完美解决。

原文地址:https://www.cnblogs.com/kekexuanxaun/p/9451179.html