环境与背景
在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
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文件
- 查看相应的堆栈信息, 以及方法入栈/出栈等信息,
系统属性
显示线程
等确定代码异常位置 - 查看类信息, 找出类创建数量多的类, 是否有自己写的相关类信息,进行代码定位
代码解决方案
- 升级groovy版本为2.4.8或以上版本
@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;
}
}
}
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