Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java动态编译优化——URLClassLoader 内存泄漏问题解决

Java动态编译优化——URLClassLoader 内存泄漏问题解决

作者头像
执笔记忆的空白
发布于 2020-12-24 02:17:22
发布于 2020-12-24 02:17:22
2.7K00
代码可运行
举报
文章被收录于专栏:Java日常Java日常
运行总次数:0
代码可运行

一、动态编译案例

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

Java URLClassLoader 动态编译案例:https://cloud.tencent.com/developer/article/1764721

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

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

二、URLClassLoader问题分析和解决

1、问题发现

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

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

2、问题分析

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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后,并没有关闭操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();

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

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

3、问题解决(三种)。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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出来的可以直接关闭,不会影响全局类的加载,具体如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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、请各位看官尊重我的劳动成果,如转载,请标明原作地址,并在评论告知我一声,谢谢~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/12/06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
java动态编译实现
本文主要实现java code 动态编译,并使用自定义的ClassLoader加载动态编译生成的字节码。
执笔记忆的空白
2020/12/24
1.9K1
JVM调优——Java动态编译过程中的内存溢出问题
由于测试环境项目每2小时内存就溢出一次, 分析问题,发现Java动态加载Class并运行那块存在内存溢出问题, 遂本地调测。
执笔记忆的空白
2020/12/24
1.6K0
Java动态编译优化——提升编译速度(N倍)
最近一直在研究Java8 的动态编译, 并且也被ZipFileIndex$Entry 内存泄漏所困扰,在无意中,看到一个第三方插件的动态编译。并且编译速度是原来的2-3倍。原本打算直接用这个插件,但是发现插件的编译源码存在我之前已经解决过的内存泄漏问题。所以拿其源码,进行改善。
执笔记忆的空白
2020/12/24
2.4K0
每日一博 - 动态编译报错 ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment
错误 java.lang.ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment 表示Java运行时环境无法找到名为 com.sun.tools.javac.processing.JavacProcessingEnvironment 的类。这个类是Java编译器API的一部分,它是用于访问编译器的内部处理环境的。
小小工匠
2024/01/06
1.2K0
每日一博 - 动态编译报错 ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment
技术分享——深入理解Java的动态编译
写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下! GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master
用户5546570
2020/06/09
1.4K0
技术分享——深入理解Java的动态编译
Groovy&Java动态编译执行
工作中,遇到部分业务经常动态变化,或者在不发布系统的前提下,对业务规则进行调整。那么可以将这部分业务逻辑改写成Groovy脚本来执行,那么就可以在业务运行过程中动态更改业务规则,达到快速响应。
一个会写诗的程序员
2018/08/17
3.1K0
jAVA不停服执行代码
尽管我们有了JAVA热更新1:Agent方式热更、JAVA热更新2:动态加载子类热更,能修复大部分线上的BUG,在项目上线之后,不可避免的会遇到出数据错乱的情况。之前的做法可能是提前写好一段代码,然后通过后台接口来进行调用,用以解决线上数据规整。但这种方式必须得提前写好规整逻辑,但不能覆盖所有情况。
Qwe7
2022/03/23
5730
玩转 Java 动态编译,太秀了~!
之前的文章 从 Spring 的环境到 Spring Cloud 的配置 中提到过我们在使用 Spring Cloud 进行动态化配置,它的实现步骤是先将动态配置通过 @Value 注入到一个动态配置 Bean,并将这个 Bean 用注解标记为 @RefreshScope,在配置变更后,这些动态配置 Bean 会被统一销毁,之后 Spring Cloud 的 ContextRefresher 会将变更后的配置作为一个新的 Spring Environment 加载进 ApplicationContext,由于 Scoped Bean 都是 Lazy Init 的,它们会在下一次使用时被使用新的 Environment 重新创建。
芋道源码
2021/07/13
1.8K0
Java动态编译优化——ZipFileIndex内存泄漏问题分析解决
前几天解决了URLClassLoader内存泄漏的问题,但是解决问题就像剥洋葱,剥去了外层,内层 问题又暴露出来了。当URLClassLoader内存泄漏解决, 需要解决的就是ZipFileIndex内存泄漏的问题了,而且这个问题折腾了我2天半的时间。
执笔记忆的空白
2020/12/24
1.5K0
java解决动态编译加载不到import的jar包
package com.*.utils; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileMana
IT工作者
2022/05/05
1.5K0
动态编译 - Dynamically Compile and Load External Java Classes
小小工匠
2024/01/09
5300
动态编译 - Dynamically Compile and Load External Java Classes
在线运行Java代码
“ 本文将探查 javax.tools 包中,并演示如何使用它们实现Java代码的在线编译 。javax.tools 包以一种通用的方式对这些概念进行了抽象化,使您能够从备用的源代码对象提供源代码,而不要求源代码必须位于文件系统中”
每天学Java
2020/06/02
4K0
java 利用反射模拟动态语言的 eval 函数
import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; public class Eval { public static Object eval(String str) throws Exception { StringBuffer
用户1177713
2018/02/24
2K0
dubbo(二)动态编译compiler
上一篇提到过@Adaptive注解的作用:被@Adaptive修饰的类实际上是一个装饰类。被@Adaptive修饰的方法则会生成一个动态代理类,而根据模板生成的类则需要通过动态编译由字节流被编译成动态代理类。本文主要讲的就是dubbo的动态编译。 dubbo-spi的扩展装饰类是通过ExtensionLoader.getAdaptiveExtension来获取,内部则进行了动态编译。核心代码如下:
虞大大
2020/09/24
1K0
Java 如何实现动态脚本?
阿里妹导读:在平台级的 Java 系统中,动态脚本技术是不可或缺的一环。本文分享了一种 Java 动态脚本实现方案,给出了其中的关键技术点,并就类重名问题、生命周期、安全问题等做出进一步讨论,欢迎同学们共同交流。
Spark学习技巧
2021/03/05
2.1K0
Java 如何实现动态脚本?
如何使用动态编译
或许大部分人工作至今都没有使用过 Java 的动态编译功能,当然我也是在机缘巧合之下才有机会去研究使用。
政采云前端团队
2023/09/01
5210
如何使用动态编译
教你如何手写一个动态代理【模拟JDK】
模拟JDK定义一个InvokeHandler public interface MyInvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 定义一个实现类 package com.jv.own; import java.lang.reflect.Method; public class MyOwnInvocationHandler imp
田维常
2020/04/26
3640
线程上下文类加载器ContextClassLoader内存泄漏隐患
今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器ContextClassLoader内存泄漏相关的Issue:
Throwable
2020/06/23
8640
线程上下文类加载器ContextClassLoader内存泄漏隐患
风控规则引擎(二):多个条件自由组合的实现,如何将 Java 字符串转换成 Java 对象
在上一篇中介绍了一个单独的动态表达式是如何执行的,这里讲一下多个表达式不同组合情况下的实现。这里主要介绍下面 2 种情况的设计,其他可自行扩展
双鬼带单
2023/12/18
6870
风控规则引擎(二):多个条件自由组合的实现,如何将 Java 字符串转换成 Java 对象
Java的脚本机制、编译器API
Java 的脚本 API 可以让我们调用 JavaScript、Grovvy、Ruby 等脚本语言,它避免了编译和链接环节,具有如下优势:
晚上没宵夜
2021/11/24
8720
相关推荐
java动态编译实现
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验