Netty长连接应用内存无法释放分析
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还没释放。
但是Finalizer线程空闲,队列为空
而Netty也明确了会通过finalize()机制进行释放内存。
3. 分析
3.1 Finalize机制
所以要研究一下finalize机制具体是如何运作的。
- 创建实现finalize()方法的对象(称为
引用对象
),JVM会创建一个java.lang.ref.Finalizer
对象进行引用。同时内部会构建Finalizer对象的双向链表。 - 当
引用对象
准备进行垃圾回收后,JVM会将Finalizer对象(引用了引用对象)加入到队列中。 - 同时名称为
Finalizer
的特殊守护线程,会循环从队列中获取Finalizer对象(调用remove方法,如果无对象返回,内部会进行阻塞) - 当获取到时,会从队列中删除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。
但是执行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的时候,才会进行设置,而此刻实际是有释放堆外内存的。
在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. 参考资料
转载自:https://juejin.cn/post/7337298499820077091