虚引用GC耗时分析优化(由 1.2 降低至 0.1 秒)
背景
线上应用频繁出现超时告警(超时时间 1 s): getUiToken 接口异常状态码“-1”出现4037次(失败描述:业务请求异常),超过阈值50,协议:http,为服务端接口。当前失败率为0%,当前平均响应时间为150ms,TP50为2ms,TP90为896ms,TP99为1024ms,TP999为1152ms,MAX为1280ms。
环境信息
-server -Djava.library.path=/usr/local/lib -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/log -Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Dnetworkaddress.cache.ttl=300 -Dsun.net.inetaddr.ttl=300 -Xms5G -Xmx5G -XX:+UseG1GC -XX:G1HeapRegionSize=4m -Xloggc:/export/log/$APP_NAME/gc_detail.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10m -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC

问题排查
分析代码实现,getUiToken 只是简单的内存数据获取并返回,没有其他复杂操作,不存在有耗时操作

接着借助 SGM 监控平台分析下接口耗时情况,发现方法执行的耗时都处在等待中,为什么会有这多长的等待时间呢?

业务代码很简单,只是内存级别的获取,序列化和反序列化也不可能耗时这么久, 难道是 GC 的原因 ? 因为 JVM 之前就配置了 -Xloggc 日志输出,所以先分析下 GC 日志情况,看看 GC 是否存在异常情况(工具 gcViewer)

果然,光看吞吐量 99.3% 和 full gc 次数 0,感觉性能好像还不错的,但 4227 次 young gc (平均 20 多秒一次) 和 max 1.25 秒的耗时,就有点离谱,分析 GC 日志详情

随机搜查了一个 GC 耗时在 1s 以上的日志,发现 1.02 s 的 gc 有 977.3 ms 是在处理 Ref Proc,为什么这个耗时会这么长 ?
ref-proc 是对各种软弱虚引用等的处理,处理 soft、weak、phantom、final、JNI 等等引用的时间
具体是那种引用类型的耗时长,可以通过增加 JVM 参数 -XX:+PrintReferenceGC 打印引用GC类型 ,重新上线一段时间后观察GC日志

发现耗时基本都在 PhantomReference 类型的对象上,为什么会有这么多这个对象?
PhantomReference 是什么? 1、虚引用也称为“幽灵引用”,它是最弱的一种引用关系。 2、如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 3、为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 4、当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。
为了搞清楚 PhantomReference 具体都是哪些对象,我们需要 dump 堆文件查看对象的分布(内存分析工具 MAT),dump 时最好将流量摘除后操作,以免影响线上。查看直方图中的对象发现有 4340 个虚引用对象,和GC日志中的数量基本对的上。

那为什么会有这么多 ConnectionPhantomReference 对象呢 ? 这个东西有什么用呢 ? 源码如下 (mysql-connector-java 版本 5.1.44)
// 相关的核心逻辑
public class NonRegisteringDriver implements java.sql.Driver {
protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>();
protected static final ReferenceQueue<ConnectionImpl> refQueue = new ReferenceQueue<ConnectionImpl>();
protected static void trackConnection(Connection newConn) {
ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);
connectionPhantomRefs.put(phantomRef, phantomRef);
}
}
public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {
public ConnectionImpl(String hostToConnectTo, int