Java的Groovy执行器内存泄露(MetaSpace)问题分析与解决办法

1 篇文章 0 订阅

环境与背景

在java程序中通过GroovyScriptEvaluator执行器创建脚本Script对象调用Groovy脚本语言来完成某些功能, ,会通过AppClassLoader或者GroovyClassLoader去生产一个随机的名称的Groovy的Script类对象,导致元数据,产生的class类会被AppClassLoader或者GroovyClassLoader内部对应的Map所引用,导致不不能满足被垃圾回收的条件, 在执行脚本期间,得到了 Out of Metaspace出错

jdk8, groovy 2.4.6版本

  <dependency>
     <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.4.6</version>
  </dependency>

执行脚本方式

1 Spring GroovyScriptEvaluator执行器
2.ScriptEngineManager 基于JDK的SPI方式提供的执行脚本引擎
3.基于GroovyShell

问题复现方式

GroovyScriptEngineImpl se;

while (true)
{
    se = new GroovyScriptEngineImpl(new GroovyClassLoader());
    CompiledScript script = se.compile("println(\"hello\")");
    script.eval(se.createBindings());
}

或者

    private static GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator();


    public static Object evaluateScript(String script, Map<String, Object> context) {
        ScriptSource scriptSource = new StaticScriptSource(script);
        return evaluator.evaluate(scriptSource, context);
    }

    public static void main(String[] args) {
        while (true) {
            evaluateScript("1+2", Maps.newHashMap());
        }
    }
   
JVM调优复现参数

-Xms200m -Xmx200m -Xss1m -XX:MetaspaceSize=40m -XX:MaxMetaspaceSize=40m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime -Xloggc:C:\Users\admin\Desktop\GC\gc-%t.log -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\admin\Desktop\GC\memory.log

类加载机制-双亲委派

Groovy的ClassLoader体系:

Bootstrap ClassLoader

sun.misc.Launcher.ExtClassLoader // 即Extension ClassLoader

sun.misc.Launcher.AppClassLoader // 即System ClassLoader

org.codehaus.groovy.tools.RootLoader // 以下为User Custom ClassLoader

groovy.lang.GroovyClassLoader

groovy.lang.GroovyClassLoader.InnerLoader

异常现象

groovy 2.4.6 版本
在这里插入图片描述
在这里插入图片描述

解决思路: 1. 更换版本groovy2.4.8以及以上版本,则会回收Script类,可以解决问题,但是会存在性能问题
在这里插入图片描述
解决思路2: 升级jdk版本, 升级jdk版本11, 尝试解决类不会回收的问题

groovy 2.4.6 版本 出现问题
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

排查Metaspace方式

1. 通过jps命令,查看java进程pid

![在这里插入图片描述](https://img-blog.csdnimg.cn/447af6724a804de4b00fcd6f11afcb90.png

2. 通过jinfo 命令查看额外的java参数配置信息

在这里插入图片描述

3. 通过jmap用来查看内存信息,实例个数以及占用内存大小

在这里插入图片描述

  • num:序号
  • instances:实例数量
  • bytes:占用空间大小
  • class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
4. 通过jstat 命令查看堆内存各部分的使用量,以及加载类的数量

评估程序内存使用及GC压力整体情况
在这里插入图片描述

S0C:第一个幸存区的大小,单位KB
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s

堆内存统计

在这里插入图片描述

NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数

jconsole 查看堆栈元数据等信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分析内存溢出信息 jvisualvm

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./ (路径)
通过参数, 导入dump文件
在这里插入图片描述
在这里插入图片描述

  1. 查看相应的堆栈信息, 以及方法入栈/出栈等信息, 系统属性 显示线程 等确定代码异常位置
  2. 查看类信息, 找出类创建数量多的类, 是否有自己写的相关类信息,进行代码定位
代码解决方案
  1. 升级groovy版本为2.4.8或以上版本
  • 1. java SPI提供的接口方式
@Slf4j
public class GroovyScriptUtil {

    public GroovyScriptUtil() {
    }

    private static final String EngineName = "groovy";
    private static ScriptEngine engine = null;
    static{
        ScriptEngineManager factory = new ScriptEngineManager();
        engine = factory.getEngineByName(EngineName);
    }
    public static Object evaluateScript(String script, Map<String, Object> params) {
        try {
            Bindings bindings = engine.createBindings();
            bindings.putAll(params);
            return engine.eval(script,bindings);
        } catch (Exception e) {
            log.error("script脚本执行异常,script:{},params:{}",script,params);
            log.error("script脚本执行异常:",e);
            return null;
        }
    }
}
  • 2.groovy shell方式
public class GroovyUtils {
    private static GroovyShell groovyShell = new GroovyShell();

    private static Map<String, Script> scriptCache = new ConcurrentHashMap<>();

    private static <T> T invoke(String scriptText, String function, Object... objects) throws Exception {
        Script script;
        String cacheKey = DigestUtils.md5Hex(scriptText);

        if (scriptCache.containsKey(cacheKey)) {
            script = scriptCache.get(cacheKey);
        } else {
            script = groovyShell.parse(scriptText);
            scriptCache.put(cacheKey, script);
        }
        
        return (T) InvokerHelper.invokeMethod(script, function, objects);
    }
}

常用的JVM参数

-XX:+HeapDumpOnOutOfMemoryError:表示当JVM发生OOM时,自动生成DUMP文件。

-XX:HeapDumpPath:表示生成DUMP文件的路径,也可以指定文件名称。

-Xms	启动应用时,JVM 堆空间的初始大小值。
-Xmx	应用运行中,JVM 堆空间的极限值。为了不消耗扩大JVM堆空间分配的开销,将此参数和-Xms这个两个值设为相等,考虑到需要开线程,将此值设置为总内存的80%.
-Xss    单个线程堆栈大小值;JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K
-Xms 的默认值是物理内存的 1/64 但小于1G,-Xmx 的默认值是物理内存的 1/4 但小于1G
-Xmn	此参数硬性规定堆空间的新生代空间大小,推荐设为堆空间大小的1/4。
-XX:+UseParNewGC 开启此参数,多个CPU可并发进行垃圾回收,可提高垃圾回收的速度。此参数和+UseParallelGC,-XX:ParallelGCThreads搭配使用。
+UseParallelGC  年轻代使用并发收集,而年老代仍旧使用串行收集 。可提高系统的吞吐量。
XX:ParallelGCThreads  年轻代并行垃圾收集的前提下(对并发也有效果)的线程数,增加并行度.此值最好配置与处理器数目相等
-Xloggc:<file>    将 GC 状态记录在文件中 (带时间戳)
JVM 参数关系到系统的性能,而其中 -XX:PermSize,-XX:MaxPermSize,-Xms,-Xmx 和 -Xmn 这 5 个参数更是直接关系到系统的性能,系统是否会出现内存溢出。
-XX:PermSize 和 -XX:MaxPermSize 分别设置应用服务器启动时,永久存储区的初始大小和极限大小;在生产环境中强烈推荐将这个两个值设置为相同的值,以避免分配永久存储区的开销,如果不进行设置-XX:MaxPermSize ,则默认值为 64M (jdk1.8之前使用)
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发
full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超
过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-
XX:PermSize代表永久代的初始容量。


-Xloggc:/path/logs/gc-%t.log	GC日志存储的文件路径,%t 表示每次启动时用时间戳命名日志文件,如 gc-2021-03-29_20-41-47.log。
-XX:+UseGCLogFileRotation	开启日志文件分割
-XX:NumberOfGCLogFiles=16	最多分割几个文件,超过之后从头开始写
-XX:GCLogFileSize=100M	每个文件存储的上限大小,超过就触发分割
-XX:+PrintGCDetails	打印GC详细信息
-XX:+PrintGCDateStamps	打印GC日期戳
-XX:+PrintTenuringDistribution	打印对象年龄分布日志,分析 GC 时的晋升情况和晋升导致的高暂停
-XX:+PrintHeapAtGC	GC 后打印堆数据,用于对比一下 GC 前后的堆内存情况,更直观
-XX:+PrintReferenceGC	打印 Reference 处理信息
-XX:+PrintGCApplicationStoppedTime	打印 STW 暂停时间
java -XX:+PrintCommandLineFlags -version   查看JVM使用什么垃圾收集器
 -XX:+HeapDumpOnOutOfMemoryError
 -XX:HeapDumpPath=./ (路径)
jps    查看java进程号
jinfo
jstat
jstack
jamp
jconsole.exe
jvisualvm.exe

调优工具Arthas

支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断
线上程序运行问题。Arthas 官方文档十分详细,详见:https://alibaba.github.io/arthas
1.用java -jar运行即可,可以识别机器上所有Java进程
2. 选择识别的java进程号, 输入即可进入arthas 的控制台
3. 输入dashboard,可以查看整个进程的运行情况,线程、内存、GC、运行环境信息
4. 输入thread可以查看线程详细情况
5. 输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
6. 使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值
7. 还有很多命令可以在官网查看,进行使用即可

参考

https://www.cnblogs.com/hanease/p/16131868.html
https://blog.csdn.net/ld851/article/details/111180179
https://blog.csdn.net/weixin_46017166/article/details/127820176
jvm: https://api.dandelioncloud.cn/article/details/1497067909796171778

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值