由于测试环境项目每2小时内存就溢出一次, 分析问题,发现Java动态加载Class并运行那块存在内存溢出问题, 遂本地调测。
一、找到动态编译那块的代码,具体如下
-
/**
-
* @MethodName : 编译java代码到Object
-
* @Description
-
* @param fullClassName 类名
-
* @param javaCode 类代码
-
* @return Object
-
* @throws IllegalAccessException
-
* @throws InstantiationException
-
*/
-
public Class javaCodeToObject(String fullClassName, String javaCode) throws IllegalAccessException, InstantiationException {
-
Object instance = null;
-
//获取系统编译器
-
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
-
// 建立DiagnosticCollector对象
-
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
-
-
// 建立用于保存被编译文件名的对象
-
// 每个文件被保存在一个从JavaFileObject继承的类中
-
ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));
-
-
List<JavaFileObject> jfiles = new ArrayList<>();
-
jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));
-
-
//使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
-
List<String> options = new ArrayList<>();
-
options.add("-encoding");
-
options.add("UTF-8");
-
options.add("-classpath");
-
options.add(this.classpath);
-
//不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
-
options.add("-XDuseUnsharedTable");
-
-
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
-
-
// 编译源程序
-
boolean success = task.call();
-
-
if (success) {
-
//如果编译成功,用类加载器加载该类
-
JavaClassObject jco = fileManager.getJavaClassObject();
-
DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);
-
Class clazz = dynamicClassLoader.loadClass(fullClassName,jco);
-
try {
-
dynamicClassLoader.close();
-
//卸载ClassLoader所加载的类
-
ClassLoaderUtil.releaseLoader(dynamicClassLoader);
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
return clazz;
-
} else {
-
//如果想得到具体的编译错误,可以对Diagnostics进行扫描
-
String error = "";
-
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
-
error = error + compilePrint(diagnostic);
-
}
-
}
-
return null;
-
}
二、本地写测试类,并且启动执行
本地动态加载1000个类,测试查看内存空间变化
-
public static void main(String[] args) {
-
-
String code = "import java.util.HashMap; " +
-
"import com.yunerp.web.vaadin.message.alert; " +
-
"import java.util.List; " +
-
"import java.util.ArrayList; " +
-
"import com.yunerp.web.vaadin.util.modularfuntion.base.BaseUtil; " +
-
"import com.yunerp.web.vaadin.util.function.TableFuntionUtil; " +
-
"import com.yunerp.web.vaadin.util.modularfuntion.stoUtil.StoUtil; " +
-
"import java.util.Map;import com.yunerp.web.vaadin.util.modularfuntion.user.mini.HomePageUtil; " +
-
"import com.yunerp.web.util.run.WebInterface; " +
-
" " +
-
"public class web2905763164651825363 implements WebInterface { " +
-
" public Object execute(Map<String,Object> param) { " +
-
" System.out.println(param.get("key"));" +
-
" return null; " +
-
" } " +
-
"}";
-
String name = "web2905763164651825363";
-
-
for(int i=0;i<1000;i++){
-
long time1 = System.currentTimeMillis();
-
DynamicEngine de = DynamicEngine.getInstance();
-
try {
-
Class cl = de.javaCodeToObject(name,code);
-
WebInterface webInterface = (WebInterface)cl.newInstance();
-
Map<String,Object> param = new HashMap<>();
-
param.put("key",i);
-
webInterface.execute(param);
-
-
}catch (Exception e) {
-
e.printStackTrace();
-
}
-
System.gc();
-
long time2 = System.currentTimeMillis();
-
System.out.println("次数:"+i+" time:"+(time2-time1));
-
}
-
}
三、使用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" ,重新编译运行,内存溢出问题解决
-
//使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
-
List<String> options = new ArrayList<>();
-
options.add("-encoding");
-
options.add("UTF-8");
-
options.add("-classpath");
-
options.add(this.classpath);
-
//不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
-
options.add("-XDuseUnsharedTable");
重新运行的效果图如下:
至此,问题完美解决。