乐趣区

关于java:MySQL-驱动中虚引用-GC-耗时优化与源码分析

本文要点:
  • 一种优雅解决 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

一位后端写码师,一位光明操持制造者。

退出移动版