本文主要讲述MySQL连接池配置不合适时,由于MySQL以虚引用的方式作为线程清理的后备手段,导致JVM年老代随时间缓慢增长,直至FullGC的问题。为了优化数据库连接池配置,使得JVM进行尽量少的FullGC导致服务故障,本文提供了多种解决方案
问题描述
发现容器服务的老年代在不断的增长,直到FullGC
回收
MAT dump分析
使用Java自带的
jmap
、jstack
,Java 8 引入的jcmd
,或Arthas heapdump 等工具,来进行Thread dump
使用MAT进行堆栈日志分析
Leak Suspect
在mysql-connector-java-5.1.49
的com.mysql.jdbc.AbandonedConnectionCleanupThread
单独的MySQL清理,负责关闭被遗弃的MySQL连接,即没有被显式关闭的连接
ConcurrentHashMap类型的connectionFinalizerPhantomRefs常量,包含了一系列的虚引用。目的是作为保底操作:当connection对象回收时,顺便回收相关资源
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
四种引用类型可见Java 垃圾收集器与内存分配策略
当创建新的连接的时候就会调用trackConnection
方法,把MysqlConnection添加到虚引用的ConcurrentHashMap中
由此可以分析出造成年老代不断增长的原因是MySQL连接短时间内不断创建回收的
weak refs processing处理逻辑
此段抄自https://www.jianshu.com/p/2db280229343
void ReferenceProcessor::process_discovered_references(BoolObjectClosure* is_alive,OopClosure* keep_alive,VoidClosure* complete_gc,AbstractRefProcTaskExecutor* task_executor) {NOT_PRODUCT(verify_ok_to_handle_reflists());assert(!enqueuing_is_done(), "If here enqueuing should not be complete");// Stop treating discovered references specially.disable_discovery();bool trace_time = PrintGCDetails && PrintReferenceGC;// Soft references{TraceTime tt("SoftReference", trace_time, false, gclog_or_tty);process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,is_alive, keep_alive, complete_gc, task_executor);}update_soft_ref_master_clock();// Weak references{TraceTime tt("WeakReference", trace_time, false, gclog_or_tty);process_discovered_reflist(_discoveredWeakRefs, NULL, true,is_alive, keep_alive, complete_gc, task_executor);}// Final references{TraceTime tt("FinalReference", trace_time, false, gclog_or_tty);process_discovered_reflist(_discoveredFinalRefs, NULL, false,is_alive, keep_alive, complete_gc, task_executor);}// Phantom references{TraceTime tt("PhantomReference", trace_time, false, gclog_or_tty);process_discovered_reflist(_discoveredPhantomRefs, NULL, false,is_alive, keep_alive, complete_gc, task_executor);}// Weak global JNI references. It would make more sense (semantically) to// traverse these simultaneously with the regular weak references above, but// that is not how the JDK1.2 specification is. See #4126360. Native code can// thus use JNI weak references to circumvent the phantom references and// resurrect a "post-mortem" object.{TraceTime tt("JNI Weak Reference", trace_time, false, gclog_or_tty);if (task_executor != NULL) {task_executor->set_single_threaded_mode();}process_phaseJNI(is_alive, keep_alive, complete_gc);}
}
看JVM源码,weak refs processing
主要包括SoftReference
、WeakReference
、FinalReference
、PhantomReference
以及JNI Weak Reference
这五种Reference对象的处理,处理的主要内容是对之前标记的Reference对象重新处理,重新判断是否需要标记(不标记就是要回收的),如果不标记就需要放到refqueue里,等待java ReferenceHandler线程处理
优化方法
调整数据库连接池配置
如果应用程序使用Hikari或任何其他连接池管理器且并在正确的配置下,这些引用将在MinorGC时被回收,不会造成问题
- 适当延长
idleTimeout
(默认值为10分钟,问题进程设置为30s) - 适当延长
maxLifetime
(数据库默认的连接空闲时间是8小时,问题进程设置为25min) - 调整空闲线程数
minimumIdle
(默认会和最大连接数一样为100)
Hikari官方强烈推荐设置
maxLifetime
,并只比数据库引擎connectionTimeout
短一些
spring:datasource:test:# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),默认:10分钟idle-timeout: 1800000# 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms,建议设置比数据库超时时长少60秒,参考MySQL wait_timeout参数(show variables like '%timeout%';) --> ) -->max-lifetime: 14400000# 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)maximum-pool-size: 100# 最小连接数minimum-idle: 10
设置JVM参数
在Java参数中设置了-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
,停止生成这些引用
删除connectionFinalizerPhantomRefs集合
数据库连接的释放有连接池保证,这个保底机制其实是多余的,可以起个定时任务线程,去清理这个connectionFinalizerPhantomRefs集合。
下面是一个旧版本的清理示例代码。
参考资料:
- MySQL AbandonedConnectionCleanupThread Memory Leak
- 记一次数据库连接池导致的OOM的问题
- 数据库连接池配置不当导致的full gc问题记录
- HikariCP