一次 groovy 导致的 oom 排查

关键词:groovyshell evaluate 内存泄露

grovvy 版本 2.4.10

1. 起因

代码在线上运行一段时间后,容器会自动重启,查看容器监控发现是内存溢出导致了自动重启。

查看 jvm 堆情况。堆内存的使用实际不大,但是 docker 实例的内存却又占满了,又因为 docker 实例并没有安装 agent 进程,说明很大可能是 JVM 堆外内存导致。

使用 jstat -gc pid 查看,发现 Metaspace 占用很大

回想该应用出现状况前的改动,主要是新增了 groovy 脚本执行任务。查看代码发现每次执行都会创建对象

public Object evaluate(String name, String script, Binding binding) {
    binding.setVariable("logger", LoggerFactory.getLogger("script-" + name));
    binding.setVariable("builder", new Builder());
    binding.setVariable("selector", this.selector);
    GroovyShell shell = new GroovyShell(binding);
    return shell.evaluate(script);
}

看监控类加载总量很大,猜测是因为生成很多临时 class 对象,无法 gc

2. 排查之后的具体原因

new GroovyShell() 未指定 patentClassloader

所以 GroovyShell 初始化的时候会自动 new 一个 classLoader 并设置 GroovyShell.class.getClassLoader() 为父 classloader

所以最终加载脚本文件的时候是交给 GroovyShell.class.getClassLoader() 处理的

执行 evaluate 方法,只传入了脚本字符串,直接 evaluate 字符串不会缓存脚本,所以每次执行都会重新加载一次,生成一个全新的 class 对象

但是 JVM 中的 Class 只有满足以下三个条件,才能被回收(也就是该 Class 被卸载 unload)

  • 类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。
  • 加载该类的 ClassLoader 已经被 GC。
  • 该类的 java.lang.Class 对象没有在任何地方被引用

由于实际负责加载类的 classloader 不会回收,导致 class 对象一直存在于 Metaspace 并且随着时间推移越来越多,并且启动时没有设置 Metaspace 最大值,导致 Metaspace 空间无限增长,最终容器自身 oom,自动重启

对于64位JVM来说,元空间的默认初始大小是20.75MB,默认的元空间的最大值是无限

评论