记一次JVM Metaspace溢出排查

多图预警!

  • 环境:系统测试(Windows Server/JRE8/tomcat7)
  • 现象:应用运行几天后,出现访问超时,服务器cpu利用率居高不下
  • 问题日志:OutOfMemoryError:MetaSpace
  • 问题分析:
    • 原因分析:MetaSpace是jvm存放类信息的内存空间,发生溢出的可能原因:

      • metaSpace设置过小,不足应用所需
      • 应用metaSpace持续增长,超过metaSpace限制
    • 定位:问题最先从DeviceStatusMonitorTask中报出,而这个定时任务新版本修改了同步设备状态的功能,主要是与vag通信获取设备状态信息。
  • 猜测:
    • 设备状态监控任务中动态生成代理类,导致metaSpace不断消耗
  • 重现:
    • 本地运行应用,添加多个可用设备,缩短task执行间隔
    • 开启Java VisualVM监控
    • 限制Metaspace最大值:-XX:MaxMetaspaceSize=100m

  

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

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

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

  Task类 run方法代码:

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

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

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

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

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

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

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

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

  • 解决方案

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

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

最终代码如下:

  • 结果验证

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

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

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

  • 总结

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

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

时间: 2024-09-30 10:21:07

记一次JVM Metaspace溢出排查的相关文章

5种JVM垃圾收集器特点和8种JVM内存溢出原因

先来看看5种JVM垃圾收集器特点 一.常见垃圾收集器 现在常见的垃圾收集器有如下几种: 新生代收集器: Serial ParNew Parallel Scavenge 老年代收集器: Serial Old CMS Parallel Old 堆内存垃圾收集器:G1 每种垃圾收集器之间有连线,表示他们可以搭配使用. 二.新生代垃圾收集器 (1)Serial 收集器 Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集.Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,

Tomcat中JVM内存溢出及合理配置

Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个Java虚拟机.Tomcat的内存溢出本质就是JVM内存溢出,所以在本文开始时,应该先对Java JVM有关内存方面的知识进行详细介绍. 一.Java JVM内存介绍 JVM管理两种类型的内存,堆和非堆.按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.""在JVM中堆之外的内存称为非堆内存(Non-heap

JVM内存溢出之tomcat配置

JVM内存溢出常见的有一下两种: 第一种:java.lang.OutOfMemoryError: PermGen space 第二种:java.lang.OutOfMemoryError: Java heap space 第一种异常原因是因为我们项目中需要加载的文件太多所导致,项目发布时class文件和jar文件会被加载到jvm的永久区,当永久区空间不足时则会抛出 java.lang.OutOfMemoryError: PermGen space: 第二种为堆栈溢出,此异常在项目运行其产生,由于

Tomcat中JVM内存溢出及合理配置(转)

Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个Java虚拟机.Tomcat的内存溢出本质就是JVM内存溢出,所以在本文开始时,应该先对Java JVM有关内存方面的知识进行详细介绍. 一.Java JVM内存介绍 JVM管理两种类型的内存,堆和非堆.按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.""在JVM中堆之外的内存称为非堆内存(Non-heap

巧解Tomcat中JVM内存溢出问题

你对Tomcat 的JVM内存溢出问题的解决方法是否了解,这里和大家分享一下,相信本文介绍一定会让你有所收获. tomcat 的JVM内存溢出问题的解决 最近在熟悉一个开发了有几年的项目,需要把数据库从mysql移植到oracle,首先把jdbc的连接指向mysql,打包放到tomcat里面,可以跑起来,没有问题,可是当把jdbc连接指向oracle的时候,tomcat就连续抛java.lang.OutOfMemoryError的错误,上网google了一下,了解了一下tomcat的运行机制,也

解决JVM内存溢出问题

今天遇到了一个问题,当我在增加配置文件(*.xml)内容的时候,重新启动tomcat6时,控制台报错:java.lang.StackOverflowError: 即,栈溢出错误. 内存溢出,即程序运行要用到的内存大于虚拟机能提供的最大内存就发生内存溢出了. 内存溢出的问题要看业务和系统大小而定. 查看jvm内存 首先我们可以查看jvm内存,在运行里面输入cmd然后输入jconsole,打开Java监视和管理控制台: 选择本地进程连接: 有3种解决JVM内存溢出的方式: 第一种,将没用的配置文件删

【转】Tomcat中JVM内存溢出及合理配置

Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个Java虚拟机.Tomcat的内存溢出本质就是JVM内存溢出,所以在本文开始时,应该先对Java JVM有关内存方面的知识进行详细介绍. 一.Java JVM内存介绍 JVM管理两种类型的内存,堆和非堆. 按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”.

jvm虚拟机(一):jvm内存溢出问题的分析与解决

??学习一下java虚拟机系列,之一 添加运行参数-XX:+HeapDumpOnOutOfMemoryError -Xms30m -Xmx30m -XX:+HeapDumpOnOutOfMemoryError 这个参数会生成堆栈快照,用于定位异常 模拟内存溢出的场景,简单代码: 123456789101112131415161718192021222324252627282930313233 package top.alertcode.demo.jvm; import java.util.Arr

生产环境-jvm内存溢出-jprofile问题排查

通常来说,分析堆内存快照(Heap Dump)是一个很好的定位手段 ,开启了dump的参数: -XX:+HeapDumpOnOutOfMemoryError 有了这个参数,当我们不得不面对内存溢出异常的时候会节约大量的时间.默认情况下,堆内存快照会保存在JVM的启动目录下名为java_pid<pid>.hprof 的文件里(在这里<pid>就是JVM进程的进程号) dump的内容有2G,先进行压缩打包,传输至本地(scp) tar -czvf dump.tar java_pid48