起因

  • 线上新运行的一个微服务,启动过程到稳定之后,服务一致没有问题,但是在运行一小段时间之后,经常服务状态不可用,访问改服务的Restful接口处于503状态

初步分析

  • 刚开始的时候并没有意识到这个问题的严重性,以为是服务器突发的问题 ,采取的手段 就是暴力重启,可是重启之后一小段时间内确实没问题,但是之后又服务不可用了
  • 通常 情况下,HTTP 503的状态码标示当前的服务响应有问题,难道是总线的通信 有问题?观察 其他在该节点上的服务状态之后发现,他们都响应的很及时,这个服务是怎么了,扑街了?
  • 这个时候的直觉告诉我,这个服务肯定是运行一段时间之后崩掉了,但是log里边为什么没有报OOM的 错误呢?查看了下服务的启动参数,发现没有显示的打印内存溢出日志,二话不说在启动参数里边配置下-XX:+HeapDumpOnOutOfMemoryError,别说,还真发现了异常日志:Java OutOfMemoryError: Metaspace

Metaspace异常

  • 拿到了异常堆栈用MAT分析工具统计了一下,发现报告中分析内存 可能泄露的地方都是在调用URLClassLoader和Sysytem的ClassLoader,不太像是程序引起的内存泄露呀
    【异常情况下的内存快照】
    异常堆栈
    【服务启动什么都不做情况下的内存快照】
    正常启动
  • 通过Jmap dump 服务正常启动之后的内存快照发现,异常场景下的快照文件中的内存并没有增加多少呀,为什么会引起内存溢出呢?
  • 虽然还是不清楚问题产生的原因,为了分析此问题,决定把启动参数的metaspace空间调大一些,再来看看正常执行结束之后dump的内存快照到底是什么样的
    【XX:MaxMetaspaceSize扩大一倍之后的快照】
    调大之后dump的 快照
  • 拿到Metaspace空间扩容之后的内存快照文件之后瞬间变得不淡定了,纳尼,怎么分析的结论还是ClassLoader的问题?而且前后感觉只增加了10M呀,启动参数XX:MaxMetaspaceSize设置的是64M,远还没有到达这个峰值呀?瞬间不淡定了。
  • 冷静下来之后分析了下这个现象,可以肯定的是,元空间(MetaspaceSize)的数据肯定发生过增加,肯定有一部分的class或者常量老化到这里,但是为什么这个空间增长的不多,却还是内存溢出了呢?莫非是发生过GC,回收掉了?但是存在矛盾呀,MetaspaceSize的数据都基本是class一些数据,还有静态常量文件,不可能会有显著的GC效果呀?

Jconsole监控内存变化

  • 为了搞定这个问题,决定要监控一下JVM的内存使用情况,这让我想到了用Jconsle来看一下整个运行过程中的元空间内存变化过程,服务启动添加如下配置参数,Jconsole连接对应的IP和端口号进行连接
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=10.10.10.111.1 -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
  • 这一连上,不得了,我的个乖乖,服务启动正常之后的内存就跑到快60M了,而且执行执行代码代码逻辑之后,元空间数据又
    老化了15M左右  在这里插入图片描述
  • 从内存监控来看 ,看来我们内存溢出确实是因为原来分配的最大元数据空间内存64M是不够用的,因为启动稳定之后都接近60M了,程序在执行过程中再老化一些数据,肯定会导致内存溢出的,这也就说明了开头的那个问题。
  • 如果规避这个问题,我们之前调大这个参数就解决了这个问题,但是现实生产环境对硬件资源的要求肯定是有限度的呀,为什么启动就需要这么大的内存?执行任务过程中为什么会老化一些数据?对于这个问题还是要刨根问底、。
  • 首先,在服务运行过程中,我们看到上图对应的Metaspace空间增长是线性的,并没有短时间内急剧增加内存,说明这个可能是正常现象,而且我们通过MAT分析了启动稳定之后和执行程序之后的对象占用,并没有发现有某一种类型的对象发生过陡峭增长。
  • 【启动稳定后Class分布】
    在这里插入图片描述
    *【执行程序之后的Class分布】
    在这里插入图片描述

优化手段

  • 在查阅资料过程中发现,JVM 有这么一个参数,-XX:MaxMetasaceFreeRatio=N ,当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间,我设置的参数为50,为了验证 他的GC情况,在启动参数中添加了-XX:+PrintGCDetails -Xloggc:…/logs/gc.log -XX:+PrintGCTimeStamps 日志,但是GC日志并没有正常数据,MaxMetasace的空间也没有看到明显的减少
  • 由于这部分空间主要放的是Class的信息,标准的甲骨文/ Sun VM旁观世界是:类是永远的。所以一旦加载,即使没有人在意,他们也会留在记忆中。我们了解到JVM 还有个配置参数-XX:+CMSClassUnloadingEnabled,他可以扫描MaxMetasace区,卸载掉不在使用的类,配置这个参数之后并没有发现较大的提升,但是可以防止重复创建class而导致的内存泄露

降低启动时MaxMetasace内存

  • 就JVM 类的加载机制而言,在加载阶段,虚拟机需要完成以下三件事情:
  • 1. 通过一个类的全限定名来获取其定义的二进制字节流。
    
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    
    3.  在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
    
  • 就第一条而言,二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。
  • 因此我们怀疑除了服务包上的class正常占用MaxMetasace空间外,很有 可能是我们引用的jar包加载了很多的class
  • 于是排查服务中引用比较大的jar包,出掉多余的加载项,对于那些不能去掉的,按照最小粒度去加载,经过调试之后发现服务启动之后的Matspace空间成功的减少了近20M!

遗留问题

  • 这里这个问题 虽然解决了,但是还有一个遗留问题,为什么 明明Metaspace占用的空间都超过60M了,dump的 快照只有30多M?
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐