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机制具体是如何运作的。

  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。

但是执行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方法,进行释放内存。

java 复制代码
//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...
相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题】【Java基础篇】第30题:JDK动态代理和CGLIB动态代理有什么区别
java·开发语言·后端·面试·代理模式
swipe2 小时前
别再把 AI 聊天做成纯文本:从 agui 这个前后端项目,拆解“可感知工具调用”的流式 AI UI
后端·langchain·llm
GetcharZp2 小时前
GitHub 爆火!纯 Go 编写的文件同步神器 Syncthing,凭什么成为程序员的标配?
后端
hERS EOUS2 小时前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
DFT计算杂谈2 小时前
wannier90 参数详解大全
java·前端·css·html·css3
LucianaiB2 小时前
我用飞书多维表做了一个 AI 活动推荐智能体:每天自动催我别错过截止日期!
后端
marsh02062 小时前
43 openclaw熔断与降级:保障系统在异常情况下的可用性
java·运维·网络·ai·编程·技术
张健11564096482 小时前
临界区和同一线程上锁
java·开发语言·jvm
铁皮饭盒3 小时前
第2课:5分钟!用 Trae AI 生成你的第一个后端服务(Bunjs + Elysia)
前端·后端·全栈
超梦dasgg3 小时前
智慧充电系统设备管理服务对外接口实现方案
java·spring·微服务