问题描述
公司的一个SpringMVC服务,最近在做运维检查的时候发现young gc 和 full gc太频繁,远远超过了正常情况。服务器配置是4核8G,该服务分配了6G内存。通过arthas的dashboard统计情况在20个小时之内发生的young gc和 full gc 次数,如下图:young gc 393次,full gc 19次
问题排查
- 在eden区达到80%的时候,通过arthas 的 heapdump dupm了堆内存文件
java -jar arthas-boot.jar
挂载对应的应用的PID,进入到arthas管理界面
heapdump /tmp/dump.hprof
- 使用MemoryAnalyzer 分析hprof 文件,发现heap中存在最多的对象就是java.lang.ref.Finalizer 对象,占用了整个空间的47%,如下图
- 点击Histogram 查看Finalizer对象,右键—>List Objects—> with outgoing reference 查看具体的Finalizer对象,查看图片左下角的referent 就是真正需要回收的垃圾。分析了一部分referent 都是涉及到底层jdk 关于https请求连接的对象。如HtppsURLConnectionIpml,ZipFileInputStream等。
- 结合线上系统的业务日志发现该服务与第三方系统交互频繁,就是通过原生的jdk 自带的HttpsURLConnection 来发起的请求。那具体是什么原因造成了大量的Finalizer对象堆积呢?
- 原因就是每次发起请求都会 new HttpURLConnection 对象,请求完毕 该对象及其关联的对象并没有立即释放内存而是进入了ReferenceQueue队列等待Finalizer 的守护线程来回收。如果主线程的请求比较频繁,就会产生大量的Finalizer对象放入到Queue中。而守护线程的优先级是比较低导致回收Finalizer对象的速度远低于主线程产生的速度,这样就导致了eden区内存迅速的被Finalizer对象占满。
- 为什么这些对象不是直接回收而是进入到Queue等待被守护线程回收呢?那是因为这些对象重写了finalize()方法,导致需要释放的对象被放入到Queue通过守护线程来回收。
HttpsURLConnectionImpl.java
protected void finalize() throws Throwable {
this.delegate.dispose();
}
protected void dispose() throws Throwable {
super.finalize();
}
解决方案
解决方案就是减少Finalizer对象的产生速度,但是业务访问是不能减少,所以我们就引入了HttpClient 现有的连接池功能,不用每次连接都new 新的对象而是复用连接池中空闲的对象。封装上线后频繁young gc的问题得到了彻底的解决。事后监控基本上几个小时发生一次young gc ,full gc更少了。
总结
- 线上环境需要一定的成熟的APM(应用性能管理) 对企业系统即时监控以实现对应用程序性能管理和故障管理的系统化的解决方案。
- 通过合理搭配工具,提高问题处理能力。
- 经过线上实际问题的处理,加深了对系统底层的了解,对以后问题排查流程更加熟练、更加自信、更加高效。
- 实践是检验巩固技术的唯一真理–诚不欺我。