Aviator 是一个高性能、轻量级的 Java 语言实现的表达式求值引擎,主要用于各种表达式的动态求值。Aviator 的实现思路与其他轻量级的求值器不同,其他求值器一般都是通过解释的方式运行,而 Aviator 则是直接将表达式编译成 Java 字节码,交给 JVM 去执行。
问题描述
今天发版,监控线上 JVM 信息。发现日志量暴涨,而同时期业务量也增长了一倍多,所以一开始并没有觉着有什么不正常的地方。
日志量暴涨
qps
Metaspace 空间暴涨
直到看到类加载的信息,发现系统在不断地加载类,导致 Metaspace 空间快速增长,频繁触发 major GC(full GC)。这是一个极其不正常的现象,因为一个系统运行一段时间之后,其加载的类数量应该是趋于稳定的,不应该存在如此大的波动。Metaspace
class-loading
原因定位
继续查看监控,发现有一个线程池的调用量暴涨,比平时多了几十倍。executor-pool
使用这个线程池的是支付路由业务,里面涉及到了表达式求值的逻辑,用到了 Aviator 框架。业务使用的方法
compile 方法的关键代码如下:
1 | /** |
在不开启缓存的情况下,innerCompile 方法将会产生大量的类加载器和内部类。这也是 class-loading 图中 class 数量一直增长的原因。
inner-compile 方法
AviatorClassLoader
ASMCodeGenerator
什么时候分配 Metaspace 空间
当一个类被加载时,它的类加载器会在 Metaspace 中分配空间用于存放这个类的元数据。 如下图所示,类加载器 Id 第一次加载类 X 和 Y 的时候,会在 Metaspace 中为它们开辟空间存放元信息。
Metaspace 分配
什么时候回收 Metaspace 空间
分配给类的 Metaspace 空间,是归属于这个类的类加载器的。只有当这个类加载器被卸载的时候,这个空间才会释放。
所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的 Metaspace 空间才会被 GC 释放。(JLS 12.7. Unloading of Classes and Interfaces)
Metaspace 回收
修复方案
今天的问题之所以发生,是因为 Aviator 框架会不断地生成新的类加载器和类。 我们只需要开启缓存,这样表达式的编译结果就会被缓存起来。下次碰到相同的表达式,直接从缓存中返回结果,不用再编译。避免了因为 Metaspace 空间快速增长而导致频繁 major GC 的问题。
修复前
修复后
小结
- 对于 JVM 里面的内存需要在启动时进行限制, 包括我们熟悉的堆内存、直接内存和 Metaspace 空间,这是保证线上服务正常运行的兜底措施。
- 对于使用了 ASM 等字节码增强工具的类库,在使用他们时请多加小心(尤其是 JDK1.8 以后)。使用类库时,多注意代码的写法,尽量不要出现明显的内存泄漏。