记一次JVM Metaspace溢出排查

时间:2023-01-02 21:44:25

多图预警!

  • 环境:系统测试(Windows Server/JRE8/tomcat7)
  • 现象:应用运行几天后,出现访问超时,服务器cpu利用率居高不下
  • 问题日志:OutOfMemoryError:MetaSpace
  • 问题分析:
    • 原因分析:MetaSpace是jvm存放类信息的内存空间,发生溢出的可能原因:
      • metaSpace设置过小,不足应用所需
      • 应用metaSpace持续增长,超过metaSpace限制
    • 定位:问题最先从DeviceStatusMonitorTask中报出,而这个定时任务新版本修改了同步设备状态的功能,主要是与vag通信获取设备状态信息。
  • 猜测:
    • 设备状态监控任务中动态生成代理类,导致metaSpace不断消耗
  • 重现:
    • 本地运行应用,添加多个可用设备,缩短task执行间隔
    • 开启Java VisualVM监控
    • 限制Metaspace最大值:-XX:MaxMetaspaceSize=100m

  记一次JVM Metaspace溢出排查

  从JVisualVM的监控视图中,我们可以直观的看出每隔一分钟都会出现线程数飙升、类加载数阶梯式增长的情况。

  随着类加载数的增长,Metaspace空间逐步从60M增长到100M,出现内存溢出,导致jvm频繁触发full GC,消耗大量CPU资源。

  •  分析——>找出问题代码

  Task类 run方法代码:

记一次JVM Metaspace溢出排查

  红框部分为新增代码,具体实现如下:

记一次JVM Metaspace溢出排查

      主要逻辑是与底层组件通信查询运行状态,然后根据结果更新状态。直接排除DAO操作的嫌疑,抽取与通信部分,整理成单独的测试代码:

记一次JVM Metaspace溢出排查

步骤: 1. 设置jvm参数 : -verbose:class  打印类加载信息

2. 清理控制台日志,调试代码。

调试过程中,我发现每次循环都会有新的类被加载:

记一次JVM Metaspace溢出排查

而这些类都是在下面这行代码运行之后加载的。

结合类加载信息以及sendRequest方法的实现,基本确认问题是由JaxbUtil处理xml、JavaBean的相互转换引起。

记一次JVM Metaspace溢出排查

继续调试分析,发现JAXBContext对象初始化时会动态加载class,而JaxbUtil每次调用都会重新创建一个JAXBContext。

记一次JVM Metaspace溢出排查

  •  解决方案

问题根因既已找到,解决思路自然清晰明确。

    考虑到jdk中已有JAXB工具类提供xml和javaBean的互转,借鉴源码发现JAXB使用弱引用Cache对象来缓存JAXBContext。

 /**
     * Cache. We don't want to prevent the {@link Cache#type} from GC-ed,
     * hence {@link WeakReference}.
     */
    private static volatile WeakReference<Cache> cache;

    /**
     * Obtains the {@link JAXBContext} from the given type,
     * by using the cache if possible.
     *
     * <p>
     * We don't use locks to control access to {@link #cache}, but this code
     * should be thread-safe thanks to the immutable {@link Cache} and {@code volatile}.
     */
    private static <T> JAXBContext getContext(Class<T> type) throws JAXBException {
        WeakReference<Cache> c = cache;
        if(c!=null) {
            Cache d = c.get();
            if(d!=null && d.type==type)
                return d.context;
        }

        // overwrite the cache
        Cache d = new Cache(type);
        cache = new WeakReference<Cache>(d);

        return d.context;
    }

结合应用的实际场景,上面的实现避免了短时间频繁创建JAXBContext。但是弱引用Cache在无引用的情况下会很快被GC回收,所以每次定时任务都会重新生成context;并且Cache对象只能存储一个context,在定时任务的运行过程中可能由于其他接口通信导致context切换。综上,JAXB的实现也无法满足当前应用的需要。

    没有现成的解决方案,只好自己写一个。

由创建JAXBContext引起问题,那就延长对象的生命周期,减少新建对象。对于相同的Class,可以使用同一个context对象与xml互相转换。由于vag的接口个数有限, 其xml报文格式并不多,因此,维护一个static Map<Class<?>, JAXBContext>来存储context对象占用的内存并不多。考虑到与vag通信属于并发执行,使用ConcurrentHashMap实现保证并发安全。

最终代码如下:

记一次JVM Metaspace溢出排查

  •  结果验证

将之前的测试代码模拟定时任务略微修改,每隔10s执行一次,重复50次。

记一次JVM Metaspace溢出排查

开启JVisualVM监视视图,从图中可以明确的看出类装载数在第一次循环时就已接近最大值,后续过程中只加载了极少数量的class,证明这种方案确实可行。

记一次JVM Metaspace溢出排查

使用修改后的代码运行整个项目,16小时后的监视图像显示:类加载数保持稳定,MetaSpace大小几乎无变化。

记一次JVM Metaspace溢出排查

  • 总结

    • 排查问题的思路适用于一般的jvm永久代或元空间溢出。
    • 主要采用单例模式的思想解决创建大量复杂对象引起的资源消耗问题。

另外,前段时间还使用-verbose:class 参数排查出Apache CXF生成的webservice客户端重复初始化引起的OOM问题的原因。客户端初始化的过程中也会根据wsdl文件动态生成class并加载,因此,使用CXF客户端代码时,应尽量使用单例模式。