今天性能测试的时候出现一个问题,接口响应时间太长达到了2s。使用JvisualVM定位问题。

1:先打开JvisualVM

java young gc 频率高 jvmfullgc频繁_堆内存

2:找到对应的应用进程(如果需要定位远程应用环境需要远程连接远程)并双击,然后进入Monitor看看CPU和堆内存是否正常,观察发现CPU正常,但是堆内存GC频繁。然后进入 Visual GC查看,发现堆内存Full GC非常频繁,并且Full GC Old区回收的内存很少(见下图2)。现在情况就非常明显了,就是内存中有大量的GC不掉的对象。下面我们看看heap里面到底是些什么东西。

java young gc 频率高 jvmfullgc频繁_压力测试_02

java young gc 频率高 jvmfullgc频繁_压力测试_03

3:切换到monitor我们点击heap dump生成dump文件(注意如果是线上生成dump文件最好不要在线上分析,应该拿下来在测试环境上分析),生成dump文件后,dump文件会自动打开,如果是线上环境根据File路径把dump文件下载下来拷贝到测试环境进行分析。

java young gc 频率高 jvmfullgc频繁_堆内存_04

java young gc 频率高 jvmfullgc频繁_java young gc 频率高_05

5:分析dump文件,点击class先根据size排序,找到比较大的实例,找到了两个比较熟悉的class,下面对这两个class进行分析。

java young gc 频率高 jvmfullgc频繁_压力测试_06

sacToNoticeConfirmRequest:这个class是我们接口:sacToNoticeConfirm的请求参数,而且现在应用程序并没有请求sacToNoticeConfirm这个接口,为啥这个class还有这么多对象呢?即为啥这些参数没有被GC掉呢?这个我也不明白,先分析其他对象

ConstraintViolationimpl:这个class是dubboRPC请求对参数进行验证时需要的类,看看这个类的实例都是些什么内容:

java young gc 频率高 jvmfullgc频繁_压力测试_07

看了多个实例后发现ConstraintViolationimpl这个class的实例对象都是因为RPC参数校验不通过生成的对象。

回过头去想我们在sacToNoticeConfirmRequest这个类里面使用了javax.validation这个包的注解,如果sacToNoticeConfirmRequest请求参数在RPC调用时参数校验不通过就会产生大量的ConstraintViolationimpl实例。我们回头继续分析sacToNoticeConfirmRequest这个class的实例:

java young gc 频率高 jvmfullgc频繁_java young gc 频率高_08

看了多个实例后,终于知道这些GC不掉的内存是哪里来的了。猜想应该是压力测试的时候没有填写参数,而我的接口用了Dubbo 的参数校验这个特性,那么在dubbo框架层就会校验这些参数,校验不通过就会产生大量的ConstraintViolationimpl实例(这些实例又引用了很多HashMap$Node实例)。最后和压力测试人员确认,确实是存在没有填参数就压测接口的情况。我又叫压力测试人员不填参数来压测,果然复现了这个结果。

现在找到问题了,就是因为使用Dubbo参数验证这个特性,在大多数请求参数校验不通过的情况下就会出现大量对象GC不掉,导致频繁FULL GC 最终导致响应时间很慢的情况。

至于为什么在这种情况下大量的对象GC不掉,这个应该是Dubbo 参数校验的bug.所以建议不要使用Dubbo的参数校验,可以使用Dubbo的本地存根(stub)这个特性来替代Dubbo的参数校验。