网站首页 > 开源技术 正文
一、动态编译案例
要说动态编译内存泄漏,首先我们先看一个案例(网上搜动态编译的资料是千篇一律,只管实现功能,不管内存泄漏,并且都恬不知耻的标识为原创!!)
Java URLClassLoader 动态编译案例:https://blog.csdn.net/huangshanchun/article/details/72835647
这篇文章和我google搜的其他文章、资料一样,属于JDK1.6以后的版本。确实能实现动态编译并加载,但是却存在严重的URLClassLoader内存泄漏的问题,并且存在SharedNameTable 和 ZipFileIndex的内存泄漏问题。
其中SharedNameTable问题我已经解决:参考
二、URLClassLoader问题分析和解决
1、问题发现
生产环境JVM的运行情况,OLD区爆满,FULlGC不停的执行,项目大概2小时挂掉了,如下图:
在使用VisualVM和 JProfile 两者工具远程分析 测试环境和生产环境的项目后,转储堆Dump文件,并转存到本地分析。 发现动态编译这块存在URLClassLoader的内存泄漏,如下图所示:
2、问题分析
URLClassLoader占了83%的内存空间,遂研究了一下动态编译这块的代码,原案例代码如下:
- import javax.tools.*;
- import java.io.File;
- import java.net.URL;
- import java.net.URLClassLoader;
- import java.util.ArrayList;
- import java.util.List;
- public class DynamicCompile {
- private URLClassLoader parentClassLoader;
- private String classpath;
- public DynamicCompile() {
- this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
- this.buildClassPath();// 存在动态安装的问题,需要动态编译类路径
- }
- private void buildClassPath() {
- this.classpath = null;
- StringBuilder sb = new StringBuilder();
- for (URL url : this.parentClassLoader.getURLs()) {
- String p = url.getFile();
- sb.append(p).append(File.pathSeparator); //路径分割符linux为:window系统为;
- }
- this.classpath = sb.toString();
- }
- /**
- * 编译出类
- *
- * @param fullClassName 全路径的类名
- * @param javaCode java代码
- *
- * @return 目标类
- */
- public Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {
- JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
- DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
- ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));
- List<JavaFileObject> jfiles = new ArrayList<>();
- jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));
- List<String> options = new ArrayList<>();
- options.add("-encoding");
- options.add("UTF-8");
- options.add("-classpath");
- options.add(this.classpath);
- 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);
- //加载至内存
- return dynamicClassLoader.loadClass(fullClassName, jco);
- } else {
- for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
- String error = compileError(diagnostic);
- throw new RuntimeException(error);
- }
- throw new RuntimeException("compile error");
- }
- }
- private String compileError(Diagnostic diagnostic) {
- StringBuilder res = new StringBuilder();
- res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
- res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
- res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
- return res.toString();
- }
- }
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出来的可以直接关闭,不会影响全局类的加载,具体如下:
- package com.yunerp.web.util.run.compile;
- import org.apache.log4j.Logger;
- import sun.misc.ClassLoaderUtil;
- import javax.tools.DiagnosticCollector;
- import javax.tools.JavaCompiler;
- import javax.tools.JavaFileObject;
- import javax.tools.ToolProvider;
- import java.io.File;
- import java.net.URL;
- import java.net.URLClassLoader;
- import java.util.ArrayList;
- import java.util.List;
- public class DynamicEngine {
- private final Logger log = Logger.getLogger(this.getClass().getName());
- /**
- * @MethodName: 创建classpath
- * @Description
- */
- private String buildClassPath() {
- StringBuilder sb = new StringBuilder();
- URLClassLoader parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
- for (URL url : parentClassLoader.getURLs()) {
- String p = url.getFile();
- sb.append(p).append(File.pathSeparator);
- }
- return sb.toString();
- }
- /**
- * @param fullClassName 类名
- * @param javaCode 类代码
- * @return Object
- * @throws IllegalAccessException
- * @throws InstantiationException
- * @MethodName : 编译java代码到Object
- * @Description
- */
- public Class javaCodeToObject(String fullClassName, final String javaCode) throws IllegalAccessException, InstantiationException {
- DynamicClassLoader dynamicClassLoader = null;
- ClassFileManager fileManager = null;
- List<JavaFileObject> jfiles = null;
- JavaClassObject jco = null;
- URLClassLoader urlClassLoader = null;
- try {
- //获取系统编译器
- JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
- // 建立DiagnosticCollector对象
- DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
- //设置系统属性
- System.setProperty("useJavaUtilZip", "true");
- // 建立用于保存被编译文件名的对象
- // 每个文件被保存在一个从JavaFileObject继承的类中
- fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));
- 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(buildClassPath());
- //不使用SharedNameTable (jdk1.7自带的软引用,会影响GC的回收,jdk1.9已经解决)
- options.add("-XDuseUnsharedTable");
- //设定使用javaUtilZip,避免zipFileIndex泄漏
- options.add("-XDuseJavaUtilZip");
- JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
- // 编译源程序
- boolean success = task.call();
- if (success) {
- //如果编译成功,用类加载器加载该类
- jco = fileManager.getJavaClassObject();
- URL[] urls = new URL[]{new File("").toURI().toURL()};
- //获取类加载器(每一个文件一个类加载器)
- urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
- dynamicClassLoader = new DynamicClassLoader(urlClassLoader);
- Class clazz = dynamicClassLoader.loadClass(fullClassName, jco);
- return clazz;
- } else {
- log.error("编译失败: "+ fullClassName);
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- //卸载ClassLoader所加载的类
- if (dynamicClassLoader != null) {
- dynamicClassLoader.close();
- ClassLoaderUtil.releaseLoader(dynamicClassLoader);
- }
- if (urlClassLoader != null) {
- urlClassLoader.close();
- }
- if (fileManager != null) {
- fileManager.flush();
- fileManager.close();
- }
- if (jco != null) {
- jco.close();
- }
- jfiles = null;
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return null;
- }
- }
重新发布后,测试1天的结果如下:
至此:URLClassLoader问题解决,JVM的 OLD区正常,项目能正常运行一周左右(之前是2-4小时就内存泄漏挂掉了)
补充说明:
1、我这里使用URLClassLoader是new的一个空文件流,为什么选择这么做,因客观原因,必须要用源代码的内存级动态编译,这样我无法获取到文件的具体全路径。
2、其实可以优化的更彻底,即我去除options参数里面的classpath,这样就能不用全局的ClassLoader了, 一般来说,只要配置了环境变量CLASSPATH,项目运行就能获取到,但是不知道是否是服务器环境问题,开发和测试环境Linux没法取到classpath,导致编译失败。所以这里我还是保留了buildClassPath()方法。但是总体效果还是很明显了,虽然我有点强迫症。只能等后续有时间了再去研究了。
3、另外,代码中我加上了关于useJavaUtilZip的配置,以为能解决ZipFileIndex的问题,但是实际上这个问题仍然存在,但是影响不是那么大,等待后续或者其他人来研究了。
4、代码规范我没去格式化了,其实应该进行格式化一下,该封装方法的还是封装一下的好。
5、请各位看官尊重我的劳动成果,如转载,请标明原作地址,并在评论告知我一声,谢谢~
猜你喜欢
- 2024-10-10 史上最全的Java面试资料——公司面试必备(含面试题答案)
- 2024-10-10 小白鞋搭配运动装,空乘制服要给员工“幸福感”
- 2024-10-10 Linux解压ZIP文件的8个使用案例(linux中解压zip格式文件命令)
- 2024-10-10 【学习打卡】P基础知识-数据类型Dictionary(字典)
- 2024-10-10 BT下载利器CloudTorrent安装教程详解
- 2024-10-10 python解包和压包(如何解压python安装包)
- 2024-10-10 拉链元素夹持!Vans 新款 Old Skool DX Zip 将于本周发售!
- 2024-10-10 夯实基础:真的清楚JDK 、JRE 与JVM之间的关系及区别?
- 2024-10-10 opencv手写数字识别:SVM和KNearest
- 2024-10-10 「B/S端开发」如何将DevExtreme添加到ASP.NET Core Angular应用
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)