为什么JVM每隔一小时执行一次Full GC_

在这里插入图片描述

随着接口自动化监控的完善,线上大多数的接口都被自动化监控系统接入,因此开发、测试人员可以及时的了解API的健康情况,监控内容包括检查API的状态码是否正常、返回的内容断言是否成功等,异常的接口会对外发出报警信息,从某个时间周期开始我们发现了报警内容的一些异常情况,每天都会收到一些接口状态码为502的报警。

在这里插入图片描述

报警信息由自动化监控系统发给钉钉报警群

根据报警的时间我们查了对应服务的日志,并没有发现有任何的异常信息,跟运维同学沟通后,在这个时间上运维那边也并没有对Tomcat、Nginx做相关操作,由于也没有业务人员反馈线上业务存在问题,于是502的问题暂时被搁置了起来,随着时间推移,502报警信息每天还是不定期的如约而至,这让报警群显得有些“不安静”,于是这个问题重新被重视起来,自动化监控的策略是每5分钟跑一次接口list、不定期报出502说明在监控执行这段时间内服务可能会有问题,由于是5分钟执行一次,所以每次502的周期、时间都不固定,这就解释了为什么报警时间、报警次数没有任何规律。如果每天接口真的存在若干次的502、那么一定会影响到这个时间上的用户体验,而这样的情况是不能够被接受的。
Nginx 502错误的原因比较多,reload会造成502、代理服务器无响应也会造成502,由于排除了运维对Nginx的操作,因此直接把目光放到了Tomcat上,由于Tomcat本身服务正常,并没有报警,所以502的原因大概率是JVM的GC导致的STW,于是我们找了JVM的监控数据。

在这里插入图片描述
在这里插入图片描述

监控信息由OpenFalcon提供

当看到gc监控后第一反应:惊呆了!!!刚好每个小时Full GC一次,对比接口报警时间后发现,报警的时间刚好能够跟gc时间对应上,Why???
从图中可以看到每间隔一个小时就会执行一次FGC,FGC后old区使用内存降低,说明内存被回收了,但是在回收之前总使用内存只有200多M,JVM设置的参数是3G,为什么在200M是FGC了呢,Why???

JVM参数为 -Xms3072m -Xmx3072m -XX:PermSize=1536M -XX:MaxNewSize=1536m -MaxPermSize=1536m

带着疑问我们先看下JVM在什么情况下会执行FGC

  1. old区满时
  2. young区存活对象复制到old区,old区内存不够时
  3. 方法区满时
  4. 调用System.gc()

FGC发生在old区使用了200M左右,排除1和2,方法区最大设置的1.5G,排除3,因此4是最大的嫌疑
如果是因为调用System.gc()方法导致的,那么我只需要禁用调System.gc(),然后在观察gc的情况就可以了,JVM提供了禁用System.gc()的方法,在JVM启动参数新增 -XX:DisableExplicitGC,然后重启Tomcat。

在这里插入图片描述

-XX:DisableExplicitGC
在JVM增加以上参数并重启

在这里插入图片描述

禁用System.gc()后的gc情况

过了一个半小时仍然没有FGC,基本可以确认是System.gc()导致的FGC,但是直接禁用会存在隐患,一些使用了堆外内存、或者某些框架依赖System.gc()禁用后会出现不可预知的问题,如果不能直接禁用System.gc,就需要找出是谁调用了System.gc方法,然后在优化。
查阅资料后发现Tomcat有个防止内存泄漏的JreMemoryLeakPreventionListener监听器,会注册一个定时器,每一个小时调用System.gc(),然而当我们反编译我们所使用的Tomcat版本后发现,这个定时器的周期已经改为Long.valueOf(Long.MAX_VALUE - 1) 因此也就排除了这个原因。

Class<?> clazz = Class.forName("sun.misc.GC");
Method method = clazz.getDeclaredMethod(
    "requestLatency",
    new Class[] {long.class});
method.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));

反射调用sun.misc.GC.requestLatency,传入latency参数为Long.valueOf(Long.MAX_VALUE - 1)

继续寻找…之后发现Java的RMI中也有每隔一个小时执行一次gc调用
image.png
image.png
image.png

  • DGC:为RMI分布式垃圾收集(DGC)提供类和接口。 当RMI服务器将对象返回给其客户端(远程方法的调用者)时,它会跟踪远程对象在客户端中的使用情况。 当客户端上没有对远程对象的引用时,或者引用的“租约”到期且未续订时,服务器会收集远程对象。
  • sun.rmi.transport.DGCClient会注册一个延时GC任务,会创建一个Daemon的线程,run方法中如果满足间隔时间就会执行System.gc()方法

我们系统中并没有单独使用了RMI协议,于是马上就想到了Dubbo,因为Dubbo是支持RMI协议的,而我们的RPC框架是使用的Dubbo。
image.png

Dubbo monitor中可以查看服务注册相关信息

因历史原因,在一些比较陈旧的系统中有使用到Dubbo的RMI协议,而所有新系统则都使用了Dubbo协议,为了验证是因为Dubbo的RMI协议引发的FGC,我们把RMI的DGC的gc时间设置为2个小时

-Dsun.rmi.dgc.client.gcInterval=7200000 -Dsun.rmi.dgc.server.gcInterval=7200000
在jvm中加入以上参数并重启服务

image.png

从上图可以看出,12点10分左右、13点10分左右间隔一小时触发了GC,在加入jvm参数并重启服务后14点第一次GC后,在16点过了2个小时后触发了第二次GC,由此确定问题是RMI的DGC导致的。

为了确定将RMI协议改为Dubbo协议后问题可以解决,于是我们将DGC时间调整为5秒,并分别使用RMI协议和Dubbo协议来看FGC的次数变化

RMI协议
image.png

可以看到每5秒一次FGC

Dubbo协议
image.png

FGC次数没有变化

问题已经定位出来了,接下来只需要将RMI协议替换成Dubbo协议,然后重启所有相关服务即可,如何找出所有使用了RMI协议的服务呢?

打开Dubbo monitor,按下F12,在Console中输入:

$('table').eq(1).find('tr').each(function(){
    var app = $(this).find('td').eq(0).html();
    $.ajax({url : 'http://DubboMonitorAddress/providers.html?application=' + app, success:function(data){
        if(data.indexOf('rmi') != -1) {
            console.log(app)
        }
    }})
});

先执行引入jquery,然后执行脚本,可以在console中看到所有使用了rmi协议的dubbo服务

在一段时间后,我们完成了所有服务的协议替换,之后在也没有出现过502报警了。

image.png

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值