记一次JVM Metaspace溢出排查

多图预警!java

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

  

  从JVisualVM的监控视图中,咱们能够直观的看出每隔一分钟都会出现线程数飙升、类加载数阶梯式增加的状况。web

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

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

  Task类 run方法代码:tomcat

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

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

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

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

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

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

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

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

  •  解决方案

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

/**

    * 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实现保证并发安全。

最终代码以下:

  •  结果验证

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

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

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

  • 总结

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

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

相关文章
相关标签/搜索