警惕动态代理导致的Metaspace内存泄漏问题

前一段时间,公司将jdk升级到1.8之后,系统出现问题,问题集中在系统中包含的两个服务上。通过监控发现,每过十五分钟,这两个服务所在的服务器的内存就会减少,不到一天的时间,整个服务器的内存就被耗光,导致两个服务出现异常。两个服务的负责人经过多次分析无果,只能将问题提给我,让我协助分析。


首先从监控的数据来看,服务器内存下降是很有规律的,每过十五分钟,可用物理内存就掉下一个台阶,跟服务开发人员了解到这两个服务之间,会通过一个定时任务同步数据,同步数据的周期正是十五分钟,因此很容易可以判断,这个问题的根源,应该就是出在定时任务上。


接下来就是查看服务的jvm内存情况,为了更好的复现问题,我让应用开发人员帮我搭建起了这两个服务的测试环境,然后将定时任务触发的频率提高为一分钟执行一次。然后通过:


jstat -gccapacity  进程号


查看服务的jvm内存分配和回收情况,结果很容易就发现导致服务器可用内存下降的原因,正是因为每次定时任务触发之后,jvm的metaspace内存都在扩张,最终导致系统可用内存被耗尽。


接下来就是分析是什么原因导致metaspace一直不断扩张,因为metaspace主要存储类元数据,所以导致其不断扩张的原因,最有可能是加载类元数据导致内存泄漏,因此我给服务启动命令加上-verbose:class启动参数,让jvm输出类装载和卸载的情况,并将服务的标准输出重定向到指定文件中,方便运行一段时间之后去检查输出的情况。


运行一段时间之后,查看输出文件,可以看到每次定时任务执行的时候,都会大量的加载Fastjson创建的代理类。检查对应的服务代码,发现两个服务在定时任务触发时,其中一个服务会通过rpc调用另外一个服务,调用参数和响应参数是json格式的,通过Fastjson进行序列化和反序列化,而出现问题的地方,正是服务通过Fastjson进行反序列化时出现。从服务的代码看,在反序列时,创建了一个ParseConfig对象用于配置Fastjson的反序列化设置,但并未指定专门的实现了ObjectDeserializer接口的反序列化器,通过分析Fastjson的源码,可以发现在这种情况下,Fastjson会默认创建一个JavaBeanDeserializer实例用于将json转换成java bean,而在转换的过程中,会首先加载这个目标java bean的代理类,加载代理类的时候通过ParseConfig对象进行检查,如果目标类已经创建了代理类,则不在创建,直接使用这个代理类来实例化bean,否则会先创建这个目标类的代理Class。Fastjson默认使用ASM来创建代理类,在未禁用ASM代理的情况下,会直接读取目标类的.class文件,然后通过Classloader的definClass1方法来将读取的字节码加载生成代理类,生成的代理类命名为Fastjson_ASM_XXX_1,其中XXX是目标类的类名,而数字1则是由一个AtomicLong递增得到。


这样就很清楚了,我们的服务在每次接口被调用时,都实例化一个ParseConfig对象来配置Fastjson反序列的设置,而未禁用ASM代理的情况下,由于每次调用ParseConfig都是一个新的实例,因此永远也检查不到已经创建的代理类,所以Fastjson便不断的创建新的代理类,并加载到metaspace中,最终导致metaspace不断扩张,将机器的内存耗尽。


定位到问题之后,解决这个问题就简单了,有很多种方法,而且每种方法都没什么难度。比如说可以定义一个全局的ParseConfig对象用于反序列化,也可以直接使用JSON.parseObject来进行反序列化,还可以通过实现ObjectDeserializer接口,自己实现反序列化的逻辑,避免让fastjson自动创建代理类。


  这个问题解决虽然简单,但是导致问题发生的原因还是值得重视。为什么在升级之前不会出现这个问题?这就要分析jdk1.8和1.7自带的hotspot虚拟机的差异了。从jdk1.8开始,自带的hostspot虚拟机取消了过去的永久区,而新增了metaspace区,从功能上看,metaspace可以认为和永久区类似,其最主要的功用也是存放类元数据,但实际的机制则有较大的不同。首先,metaspace默认的最大值是整个机器的物理内存大小,所以metaspace不断扩张会导致java程序侵占系统可用内存,最终系统没有可用的内存;而永久区则有固定的默认大小,不会扩张到整个机器的可用内存。当分配的内存耗尽时,两者均会触发full gc,但不同的是永久区在full gc时,以堆内存回收时类似的机制去回收永久区中的类元数据(Class对象),只要是根引用无法到达的对象就可以回收掉,而metaspace判断类元数据是否可以回收,是根据加载这些类元数据的Classloader是否可以回收来判断的,只要Classloader不能回收,通过其加载的类元数据就不会被回收。这也就解释了我们这两个服务为什么在升级到1.8之后才出现问题,因为在之前的jdk版本中,虽然每次调用fastjson都创建了很多代理类,在永久区中加载类很多代理类的Class实例,但这些Class实例都是在方法调用是创建的,调用完成之后就不可达了,因此永久区内存满了触发full gc时,都会被回收掉,而使用1.8时,因为这些代理类都是通过主线程的Classloader加载的,这个Classloader在程序运行的过程中永远也不会被回收,因此通过其加载的这些代理类也永远不会被回收,这就导致metaspace不断扩张,最终耗尽机器的内存了。


  其实这个问题并不局限于fastjson,只要是需要通过程序加载创建类的地方,就有可能出现这种问题。尤其是在框架中,往往大量采用类似ASM、javassist等工具进行字节码增强,而根据上面的分析,在jdk1.8之前,因为大多数情况下动态加载的Class都能够在full gc时得到回收,因此不容易出现问题,也因此很多框架、工具包并没有针对这个问题做一些处理,一旦升级到1.8之后,这些问题就可能会暴露出来。

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
JVM动态生成导致的内存溢出是一常见的问题。当使用Java反射、字节码操作库或动态代理等技术在运行时动态生成大量时,导致JVM中的永久代(在Java 7及之前的版本)或元空间(在Java 8及之后的版本)的内存占用过高,最终引发内存溢出错误。 这种情况下,由于动态生成的较多,JVM需要为每个分配一定的内存空间。如果动态生成的数量过多或者占用的内存空间过大,就导致内存溢出错误。 为了解决这个问题,可以尝试以下几个方法: 1. 增加永久代/元空间的大小:可以通过调整JVM参数,增加永久代/元空间的大小,以容纳更多的动态生成。例如,在Java 8及之后的版本中,可以使用`-XX:MetaspaceSize`和`-XX:MaxMetaspaceSize`参数来设置元空间的初始大小和最大大小。 2. 优化代码逻辑:检查代码中是否存在频繁动态生成的操作,并尝试优化这部分代码逻辑。如果可能的话,可以考虑将生成的缓存起来,避免重复生成。 3. 使用软引用/弱引用:对于动态生成的,可以尝试使用软引用或弱引用来管理它们。这样,当内存不足时,JVM可以自动回收这些,避免内存溢出错误。 需要注意的是,Java 8及之后的版本使用元空间代替了永久代,因此调整的是元空间的大小。另外,不同的JVM实现可能对动态生成的处理方式有所不同,因此在解决这问题时,需要根据具体情况选择合适的解决方案。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值