likes
comments
collection
share

Netty长连接应用内存无法释放分析

作者站长头像
站长
· 阅读数 27

Netty长连接应用内存无法释放分析

1. 背景

项目是长连接应用,但是只做调度功能,tls握手完后,返回信息就将长连接关闭。发现限制了堆内存大小,但是实际RES内存超出比较多,最后可能会导致操作系统级别OOM KILL。

JVM配置:-Xmx2g -Xms2g -XX:ReservedCodeCacheSize=48m -XX:MaxDirectMemorySize=512m -XX:MaxMetaspaceSize=96m -XX:CompressedClassSpaceSize=32m -Xss512k

netty版本:4.1.29.Final

netty-tcnative版本:2.0.14.Final

SslProvider:OPENSSL(netty支持三种策略:JDK、OPENSSL、OPENSSL_REFCNT)

2. 现象

根据以上配置,进行一波压测,压测结束后,RES内存大概在3.4G左右,导出内存文件,发现有一些OpenSSLEngine还没释放。

Netty长连接应用内存无法释放分析 但是Finalizer线程空闲,队列为空

Netty长连接应用内存无法释放分析

Netty长连接应用内存无法释放分析 而Netty也明确了会通过finalize()机制进行释放内存。

Netty长连接应用内存无法释放分析

3. 分析

3.1 Finalize机制

所以要研究一下finalize机制具体是如何运作的。

  1. 创建实现finalize()方法的对象(称为引用对象),JVM会创建一个java.lang.ref.Finalizer对象进行引用。同时内部会构建Finalizer对象的双向链表。
  2. 引用对象准备进行垃圾回收后,JVM会将Finalizer对象(引用了引用对象)加入到队列中。
  3. 同时名称为Finalizer的特殊守护线程,会循环从队列中获取Finalizer对象(调用remove方法,如果无对象返回,内部会进行阻塞)
  4. 当获取到时,会从队列中删除Finalizer对象,移除Finalizer的双向链表,然后调用引用对象的finalize()方法,最后Finalizer引用置为null。

结合GC情况,实际当第一次触发GC的时候,只会触发第2步操作,将Finalizer对象加入到队列中。

需要第二次GC的时候,才能把那些已经处理好的对象(引用对象已经没有Finalizer对象引用,而Finalizer对象也从双向链表中移除),真正的回收掉。

而实际的情况是,有些对象因为多次回收已经晋升为老年代,这种情况下必须要触发Full GC才能真正回收掉。但是正常运行的服务,期望是尽可能少的Full GC,会导致系统停顿。所以大概率会有一批对象在老年代中一直没发回收掉。

所以finalize机制存在不少弊端,不能使用,java9之后废弃了该方法,主要有以下弊端

  • 无法知道finialize()方法何时执行;可能会出现队列堆积,导致OOM(一直生成finalize对象情况下)。
  • 垃圾收集算法依赖JVM,不同的JVM实现可能表现不一样。
  • 在构建和销毁包含非空finalize方法的对象时(如果finalize为空,则JVM不会额外处理),JVM需要做更多的操作,对性能有影响。
  • 如果finalize方法执行出现异常,会是对象处于损坏,并且不会有通知。

参考资料:www.baeldung.com/java-finali…、Java源码

因此前面的问题也就容易解释了,OpenSslEngine可能存在老年代中,所以young GC没法回收掉,必须要通过两次Full GC才能回收掉。当执行两次Full GC(通过--live参数导出两次dump文件)导出的dump文件,确实没有了OpenSslEngine。

Netty长连接应用内存无法释放分析

但是执行Full GC后,RES内存并没有降低。

3.2 OPENSSL_REFCNT

不论是SslProvider=OPENSSL还是OPENSSL_REFCNT,通过FULL GC(System.gc或dump前,通过--live)都没办法使RES内存降低。

SslProvider还有一种OPENSSL_REFCNT的方式,此方式不依赖finalize()方法进行回收内存,需要显式的释放内存。压测后发现,RES内存仍然占用比较大,大概3.4G,不过dump文件中确实没有了ReferenceCountedOpenSslEngine的对象(OPENSSL_REFCNT创建的不是OpenSslEngine)。

不过即使是SslProvider=OPENSSL的方式,在JVM调用finalize()方法前,内部应该也有显式调用release操作的,因为OpenSslEngine中的对象,有发现destroyed=1,只要在release的时候,才会进行设置,而此刻实际是有释放堆外内存的。

Netty长连接应用内存无法释放分析

在release的逻辑中,内部实际会真正调用到shutdown方法,进行释放内存。

//ReferenceCountedOpenSslEngine#shutdown
public final synchronized void shutdown() {
    if (!destroyed) {
        destroyed = true;
        engineMap.remove(ssl);
        SSL.freeSSL(ssl);
        ssl = networkBIO = 0;

        isInboundDone = outboundClosed = true;
    }

    // On shutdown clear all errors
    SSL.clearError();
}

不太清楚Netty作者通过finalize()方法来释放内存的用意,可能是想做个兜底吧?

3.3 JDK

Netty的OPENSSL会通过JNI来申请堆外内存,SslProvider还提供了JDK的方式。经过压测JDK版本内存比较符合预期,在2.6G左右(线程、meta数据、netty堆外内存480M左右),但是TLS握手耗时大大增加,也证明了JDK的SSL性能比较差。

如果是为了稳定内存,而牺牲性能,当前来看并不值得。

3.4 猜测

从上述情况来看,使用OpenSSL会导致内存增大。受这篇文章启发,猜测是Native Code申请内存导致:pdai.tech/md/java/jvm…

因为OpenSSL明确了内部会用JNI进行申请内存,可能是内存分配器的缘故,这部分内容不在深入分析。

3.5 注意事项

最后,用Netty的OpenSSL需要注意版本,有些版本可能会引入内存泄漏的问题,如:

4. 结论

  • 使用Netty的OpenSSL,并没有出现内存泄漏,可能是因为内存分配器的缘故,导致堆外内存看起来比较大。
  • 如果对内存限制要求比较高,且对性能无要求,可以考虑使用JDK。

5. 参考资料

  1. www.baeldung.com/java-finali…
  2. pdai.tech/md/java/jvm…
  3. cdf.wiki/posts/11864…
转载自:https://juejin.cn/post/7337298499820077091
评论
请登录