本文要点:
- 一种优雅解决 MySQL 驱动中虚援用导致 GC 耗时较长问题的解决办法
- 虚援用的作用与应用场景
- MySQL 驱动源码中的虚援用剖析
背景
在之前文章中写过 MySQL JDBC 驱动中的虚援用导致 JVM GC 耗时较长的问题 (能够看这里),在驱动代码(mysql-connector-java 5.1.38 版本)中 NonRegisteringDriver 类有个虚援用汇合 connectionPhantomRefs 用于存储所有的数据库连贯,NonRegisteringDriver.trackConnection 办法负责把新创建的连贯放入汇合,虚援用随着工夫积攒越来越多,导致 GC 时解决虚援用的耗时较长,影响了服务的吞吐量:
public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
...
NonRegisteringDriver.trackConnection(this);
...
}
public class NonRegisteringDriver implements Driver {
...
protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap();
protected static void trackConnection(com.mysql.jdbc.Connection newConn) {ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl)newConn, refQueue);
connectionPhantomRefs.put(phantomRef, phantomRef);
}
...
}
尝试缩小数据库连贯的生成速度,来升高虚援用的数量,然而成果并不现实。最终的解决方案是通过反射获取虚援用汇合,利用定时工作来定期清理汇合,防止 GC 解决虚援用耗时较长。
// 每两小时清理 connectionPhantomRefs,缩小对 mixed GC 的影响
SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> {
try {Field connectionPhantomRefs = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
connectionPhantomRefs.setAccessible(true);
Map map = (Map) connectionPhantomRefs.get(NonRegisteringDriver.class);
if (map.size() > 50) {map.clear();
}
} catch (Exception e) {log.error("connectionPhantomRefs clear error!", e);
}
}, 2, 2, TimeUnit.HOURS);
利用定时工作清理虚援用成果空谷传声,每日几亿申请的服务 mixed GC 耗时只有 10 – 30 毫秒左右,零碎也很稳固,线上运行将近一年没有任何问题。
优化——由暴力破解到优雅配置
最近又有共事遇到雷同的问题,应用的 mysql-connector-java 版本与咱们应用的版本统一,查看最新版本(8.0.32)的代码发现对数据库连贯的虚援用有新的解决形式,不像老版本(5.1.38)中每一个连贯都会生成虚援用,而是能够通过参数来管制是否须要生成。类 AbandonedConnectionCleanupThread
的相干代码如下:
// 动态变量通过 System.getProperty 获取配置
private static boolean abandonedConnectionCleanupDisabled = Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");
public static boolean getBoolean(String name) {return parseBoolean(System.getProperty(name));
}
protected static void trackConnection(MysqlConnection conn, NetworkResources io) {
// 判断配置的属性值来决定是否须要生成虚援用
if (!abandonedConnectionCleanupDisabled) {
···
ConnectionFinalizerPhantomReference reference = new ConnectionFinalizerPhantomReference(conn, io, referenceQueue);
connectionFinalizerPhantomRefs.add(reference);
···
}
}
mysql-connector-java 的维护者应该是留神到了虚援用对 GC 的影响,所以优化了代码,让用户能够自定义虚援用的生成。
有了这个配置,就能够在启动参数上设置属性:
java -jar app.jar -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
或者在代码里设置属性:
System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");
当 com.mysql.cj.disableAbandonedConnectionCleanup=true 时,生成数据库连贯时就不会生成虚援用,对 GC 就没有任何影响了。
倡议还是应用第一种形式,通过启动参数配置更灵便一点。
什么是虚援用
有些读者看到这里晓得 mysql-connector-java 生成的虚援用对 GC 有一些副作用,然而还不太理解虚援用到底是什么,有什么作用,这里咱们在虚援用上做一点点拓展。
Java 虚援用(Phantom Reference)是 Java 中一种非凡的援用类型,它是最弱的一种援用。与其余援用不同,虚援用并不会影响对象的生命周期,也不会影响对象的垃圾回收。虚援用次要用于在对象被回收时收到零碎告诉,以便在回收时执行一些必要的清理工作。
上述虚援用的定义还是比拟难了解,咱们用代码来辅助了解:
先来生成一个虚援用:
// 虚援用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 关联对象
Object o = new Object();
// 调用构造方法生成一个虚援用 第一个参数就是关联对象 第二个参数是关联队列
PhantomReference<Object> phantomReference = new PhantomReference<>(o, queue);
// 执行垃圾回收
System.gc();
// 延时确保回收结束
Thread.sleep(300L);
// 当 Object o 被回收时能够从虚援用队列里获取到与之关联的虚援用 这里就是 phantomReference 这个对象
Reference<?> poll = queue.poll();
虚援用的构造方法须要两个入参,第一个就是关联的对象、第二个是虚援用队列 ReferenceQueue。虚援用须要和 ReferenceQueue 配合应用,当对象 Object o 被垃圾回收时,与 Object o 关联的虚援用就会被放入到 ReferenceQueue 中。通过从 ReferenceQueue 中是否存在虚援用来判断对象是否被回收。
咱们再来了解上面对虚援用的定义,虚援用不会影响对象的生命周期,也不会影响对象的垃圾回收。如果上述代码里的 phantomReference 是一个一般的对象,那么在执行 System.gc() 时 Object o 肯定不会被回收掉,因为一般对象持有 Object o 的强援用,还不会被作为垃圾。这里的 phantomReference 是一个虚援用的话 Object o 就会被间接回收掉。而后会将关联的虚援用放到队列里,这就是虚援用关联对象被回收时会收到零碎告诉的机制。
一些实际能力很强的读者会复制上述代码去运行,发现垃圾回收之后队列里并没有虚援用。这是因为 Object o 还在栈里,属于是 GC Root 的一种,不会被垃圾回收。咱们能够这样改写:
static ReferenceQueue<Object> queue = new ReferenceQueue<>();
public static void main(String[] args) throws InterruptedException {PhantomReference<Object> phantomReference = buildReference();
System.gc();Thread.sleep(100);
System.out.println(queue.poll());
}
public static PhantomReference<Object> buildReference() {Object o = new Object();
return new PhantomReference<>(o, queue);
}
不在 main 办法里实例化关联对象 Object o,而是利用一个 buildReference 办法来实例化,这样在执行垃圾回收的时候,Object o 曾经出栈了,不再是 GC Root,会被当做垃圾来回收。这样就能从虚援用队列里取出关联的虚援用进行后续解决。
关联对象真的被回收了吗
执行完垃圾回收之后,咱们的确能从虚援用队列里获取到虚援用了,咱们能够思考一下,与该虚援用关联的对象真的曾经被回收了吗?
应用一个小试验来摸索答案:
public static void main(String[] args) {ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
PhantomReference<byte[]> phantomReference = new PhantomReference<>(new byte[1024 * 1024 * 2], queue);
System.gc();Thread.sleep(100L);
System.out.println(queue.poll());
byte[] bytes = new byte[1024 * 1024 * 4];
}
代码里生成一个虚援用,关联对象是一个大小为 2M 的数组,执行垃圾回收之后尝试再实例化一个大小为 4M 的数组。如果咱们从虚援用队列里获取到虚援用的时候关联对象曾经被回收,那么就能失常申请到 4M 的数组。(设置堆内存大小为 5M -Xmx5m -Xms5m)
执行代码输入如下:
java.lang.ref.PhantomReference@533ddba
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.ppphuang.demo.phantomReference.PhantomReferenceDemo.main(PhantomReferenceDemo.java:15)
从输入能够看到,申请 4M 内存的时候内存溢出,那么问题的答案就很显著了,关联对象并没有被真正的回收,内存也没有被开释。
再做一点小小的革新,实例化新数组的之前将虚援用间接置为 null,这样关联对象就能被真正的回收掉,也能申请足够的内存:
public static void main(String[] args) {ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
PhantomReference<byte[]> phantomReference = new PhantomReference<>(new byte[1024 * 1024 * 2], queue);
System.gc();Thread.sleep(100L);
System.out.println(queue.poll());
// 虚援用间接置为 null
phantomReference = null;
byte[] bytes = new byte[1024 * 1024 * 4];
}
如果咱们应用了虚援用,然而没有及时清理虚援用的话可能会导致内存泄露 。
虚援用的应用场景——mysql-connector-java 虚援用源码剖析
读到这里置信你曾经理解了虚援用的一些根本状况,那么它的应用场景在哪里呢?
最典型的场景就是最开始写到的 mysql-connector-java 里解决 MySQL 连贯的兜底逻辑。用虚援用来包装 MySQL 连贯,如果一个连贯对象被回收的时候,会从虚援用队列里收到告诉,如果有些连贯没有被正确敞开的话,就会在回收之前进行连贯敞开的操作。
从 mysql-connector-java 的 AbandonedConnectionCleanupThread 类代码中能够发现并没有应用原生的 PhantomReference 对象,而是应用的是包装过的 ConnectionFinalizerPhantomReference,减少了一个属性 NetworkResources,这是为了不便从虚援用队列中的虚援用上获取到须要解决的资源。包装类中还有一个 finalizeResources 办法,用来敞开网络连接:
private static class ConnectionFinalizerPhantomReference extends PhantomReference<MysqlConnection> {
// 搁置须要 GC 后后置解决的网络资源
private NetworkResources networkResources;
ConnectionFinalizerPhantomReference(MysqlConnection conn, NetworkResources networkResources, ReferenceQueue<? super MysqlConnection> refQueue) {super(conn, refQueue);
this.networkResources = networkResources;
}
void finalizeResources() {if (this.networkResources != null) {
try {this.networkResources.forceClose();
} finally {this.networkResources = null;}
}
}
}
AbandonedConnectionCleanupThread 实现了 Runnable 接口,在 run 办法里循环读取虚援用队列 referenceQueue 里的虚援用,而后调用 finalizeResource 办法来进行后置的解决,防止连贯泄露:
public void run() {while(true) {
try {
...
Reference<? extends MysqlConnection> reference = referenceQueue.remove(5000L);
if (reference != null) {
// 强转为 ConnectionFinalizerPhantomReference
finalizeResource((ConnectionFinalizerPhantomReference)reference);
}
...
}
}
}
private static void finalizeResource(ConnectionFinalizerPhantomReference reference) {
try {
// 兜底解决网络资源
reference.finalizeResources();
reference.clear();} finally {
// 移除虚援用 防止可能造成的内存溢出
connectionFinalizerPhantomRefs.remove(reference);
}
}
如果你心愿在某些对象被回收的时候做一些后置工作,能够参考 mysql-connector-java 中的一些实现逻辑。
总结
本文简述了一种优雅解决 MySQL 驱动中虚援用导致 GC 耗时较长问题的解决办法、也依据本人的了解讲述了虚援用的作用、联合 MySQL 驱动的源码形容了虚援用的应用场景,心愿对你能有所帮忙。
公众号:DailyHappy
一位后端写码师,一位光明操持制造者。