metaspace内存溢出问题

1.案例背景

在系统测试过程中,在平稳运行一段时间后,测试人员反馈营运车辆报警与数据服务组件数据服务(cvalarm-data)的业务功能不可用,请求研发协助排查。
发现异常后,我首先查看运管平台-状态监控,检查服务运行情况,发现状态显示服务运行正常,初步判断可能是服务假死造成的,而能造成服务假死的多半是jvm出问题了。查看运管日志,发现异常日志出现Java heap space,即jvm堆内存溢出。本文将介绍一下分析思路与处理过程。

2.排查思路

作为一个java开发,工作或者学习过程中多多少少会了解些java虚拟机的内存结构,而Metaspace(元空间)是java虚拟机中用于存放被加载的class信息的。
由于之前工作经历中遇到java.lang.OutOfMemoryError: Metaspace的问题基本都是JVM
启动参数-XX:MaxMetaspaceSize设定过小,jvm一次性加载了过多的class导致的溢出问
题。
我首先猜想可能是设置的MaxMetaspaceSize参数太小。

2.1 检查MaxMetaspaceSize参数设置

登录运管-状态监控页面,找到cvalarm组件进入服务参数配置页面,查看组件服务MaxMetaspaceSize参数值。查看Metaspace内存最大值为128M.
这里,是否增大MaxMetaspaceSize参数值就可以解决java.lang.OutOfMemoryError:Metaspace问题呢?当然!增大内存空间可以暂时解决这个问题。但是,在这个案例中有一个重要的前提,即该异常是在正常运行一段时间后报错的。如果是一次加载过多class导致的,不是应该在服务启动时就会报内存溢出异常吗?那么,服务启动加载大量的class只是其中一个原因,还有一个因素导致了服务在运行期间依然不断的加载class文件。

2.2 异常信息收集

根据组件启动脚本可知,在JAVA_OPTIONS变量中已配置了HeapDumpOnOutOfMemoryError参数,当组件服务内存溢出时,将会存储dump文件。

	-XX:+HeapDumpOnOutOfMemoryError

metaspace内存溢出问题_第1张图片

2.3 DUMP文件分析

获取Dump文件后,使用工具MAT(MemoryAnalyzer)加载dump文件进行内存分析。
metaspace内存溢出问题_第2张图片
通过MAT工具的Leak Suspects,该模块会自动分析内存溢出可疑点并给出一份可疑的分析报告。
metaspace内存溢出问题_第3张图片
metaspace内存溢出问题_第4张图片
metaspace内存溢出问题_第5张图片
从上面的截图中可以得出如下关键信息点:

  • 可以明确,发生内存溢出时,metaspace当时内存大小为100MB。
  • 其中一个类型为sun.misc.Launcher$ AppClassLoader的类加载器所持有的引用对象占用了23.03%的内存。
  • 其中,名称为org.springframework.beans.CachedIntrospectionResults对象所持有的引用对象占用了15.44%的内存。
  • 关于CachedIntrospectionResults对象
    Spring中用于存储Java类的JavaBeans信息的缓存对象
  • 关于sun.misc.Launcher$AppClassLoader类加载器
    AppClassLoader应用类加载器,负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。
    metaspace内存溢出问题_第6张图片

2.4本地模拟调试

2.4.1 设置jvm启动参数

  • 监控jvm加载class文件顺序需要增加jvm参数-XX:+TraceClassLoading。

  • 开启NMT监控jvm内存变化,-XX:NativeMemoryTracking=detail

      -XX:+TraceClassLoading//打印类加载顺序
      -XX:+TraceClassUnloading//打印类卸载顺序
      -XX:NativeMemoryTracking=detail
      -XX:MetaspaceSize=64M
      -XX:MaxMetaspaceSize=128M
      -Xms512M
      -Xmx1024M
      -Xmn192M
      -XX:CICompilerCount=2
      -XX:+UseG1GC
      -XX:ConcGCThreads=1
      -XX:ParallelGCThreads=4
      -XX:+ExplicitGCInvokesConcurrent
      -XX:SurvivorRatio=8
      -XX:+HeapDumpOnOutOfMemoryError
      -XX:+DisableExplicitGC
      -XX:+PrintGCDetails
      -XX:+PrintGCDateStamps
      -XX:+PrintGCTimeStamps
    
  • 打印加载的class至日志文件

      C:\Users\zhouguangli\Desktop\新建文件夹\cvalarm-data——metaspace内存溢出\监控class变化\cvalarm-data-console.txt
    

2.4.2 本地监控cvalarm-data服务内存变化

  • 本地启动cvalarm-data服务
    metaspace内存溢出问题_第7张图片

  • 建立NMT内存基线
    使用jcmd指令建立NMT内存基准

      jcmd 11544 VM.native_memory baseline
    
  • 查看服务启动完成后初始状态
    待服务启动完成后,查看服务内存状态。从下图可知,元空间大小为58458112Byte(55.75MB),已加载类总数为11650个class。
    metaspace内存溢出问题_第8张图片

  • 持续观察一天,查看服务内存状态
    元空间大小为61079552Byte(58.25MB),已加载类总数为11668个class
    metaspace内存溢出问题_第9张图片
    也可通过jcmd指令查看内存状态,通过之前建立的内存基线与当前内存进行比对。输入jcmd指令,并打印至memory.txt文件中显示结果

      C:\Users\zhouguangli>jcmd 11544 VM.native_memory detail.diff scale=MB>C:\Users\zhouguangli\Desktop\新建文件夹\cvalarm-data—metaspace内存溢出\监控class变化\memory.txt
    

显示结果
metaspace内存溢出问题_第10张图片
从上图比对结果可了解到,在服务平稳运行期间且没有进行任何页面功能操作的情况下,已加载的class增加了18个,元空间内存增加了14MB.
在本文2.4.1中我们设置了打印加载class的jvm参数,接下来通过日志具体查看到底是增加了哪些class

  • 查看增加的class
    打开cvalarm-data-console.txt,查看日志。
    metaspace内存溢出问题_第11张图片
    这里我截取了部分日志,从日志中可看出,服务运行期间依然有class在加载,其中多次出现类似[Loaded sun.re€ect.GeneratedMethodAccessor fromJVM_DeneClass]这样的内容。

2.5 定位问题

关于sun.re€ect.GeneratedMethodAccessor,本文不做细致讨论,简单来说这是Sun的JDK1.6之后版本中关于方法反射调用的一个实现细节,为了提高多次反射调用时的效率而在运行时生成专用的Java类实现调用逻辑。有兴趣的可以到OpenJDK 6的Mercurial仓库看:OpenJDK 的MethodAccessorGenerator。它的基本工作就是在内存里生成新的专用Java类,并将其加载。去阅读源码的话,可以看到MethodAccessorGenerator是如何生成一个动态代理类,而这个代理类的名称命名规则正是“GeneratedMethodAccessor+数字”。到此我们可以判断,服务中应该是存在大量通过反射生成的代理类加载至内存,如果不卸载这些代理类,元空间溢出是不可避免的。

3 解决方案

但是对于java项目来说不可避免会使用到很多第三方类库,像spring、springboot、mybatis框架大多是生成代理类进行逻辑处理。

  • 增加元空间内存大小
  • 尽量使用自定义类加载器加载,只有当类加载器被回收,由该类加载器加载的class才
    能被卸载

你可能感兴趣的:(基础内容)