leak(三)cleaner【yetdone】
0
0.1
https://www.cnblogs.com/exmyth/p/14205361.html
java.nio提供的DirectByteBuffer提供了sun.misc.Cleaner类的clean()方法,进行系统调用释放堆外内存,触发clean()方法的情况有2种
- (1) 应用程序主动调用
ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
- (2) 基于GC回收
Cleaner类继承了java.lang.ref.Reference,GC线程会通过设置Reference的内部变量(pending变量为链表头部节点,discovered变量为下一个链表节点),将可被回收的不可达的Reference对象以链表的方式组织起来
Reference的内部守护线程从链表的头部(head)消费数据,如果消费到的Reference对象同时也是Cleaner类型,线程会调用clean()方法(Reference#tryHandlePending())
介绍noCleaner策略之前,需要先理解带有Cleaner对象的DirectByteBuffer在初始化时做了哪些事情:
只有在DirectByteBuffer(int cap)构造方法中才会初始化Cleaner对象,方法中检查当前内存是否超过允许的最大堆外内存(可由-XX:MaxDirectMemorySize配置)
如果超出,则会先尝试将不可达的Reference对象加入Reference链表中,依赖Reference的内部守护线程触发可以被回收DirectByteBuffer关联的Cleaner的run()方法
如果内存还是不足, 则执行 System.gc(),触发full gc,来回收堆内存中的DirectByteBuffer对象来触发堆外内存回收,如果还是超过限制,则抛出java.lang.OutOfMemoryError(代码位于java.nio.Bits#reserveMemory()方法)
而Netty在4.1引入可以noCleaner策略:创建不带Cleaner的DirectByteBuffer对象,这样做的好处是绕开带Cleaner的DirectByteBuffer执行构造方法和执行Cleaner的clean()方法中一些额外开销,当堆外内存不够的时候,不会触发System.gc(),提高性能
hasCleaner的DirectByteBuffer和noCleaner的DirectByteBuffer主要区别如下:
-
构造器方式不同:
noCleaner对象:由反射调用 private DirectByteBuffer(long addr, int cap)创建
hasCleaner对象:由 new DirectByteBuffer(int cap)创建 -
释放内存的方式不同
noCleaner对象:使用 UnSafe.freeMemory(address);
hasCleaner对象:使用 DirectByteBuffer 的 Cleaner 的 clean() 方法
读到这里,也许有读者会问,如果Netty基于hasCleaner策略,通过GC触发Cleaner.clean(),自动回收堆外内存,是不是就可以不用考虑ByteBuf.release()方法的调用,不会内存泄漏?
当然不是,一方面原因是自动触发不实时:需要ByteBuffer对象被GC线程回收才会触发,如果ByteBuffer对象进入老年代后才变得可回收,则需要等到发送频率较低老年代GC才会触发
因为堆巨大无比8g,而直接内存只有2g,堆得gc频率较低,没等堆gc,直接内存先满了,判断线上应该
1)使用了hasCleaner(因为nocleaner会直接爆掉,没有cleaner会释放它,而在uat并没有像1.3那样精准的爆掉,意味着有jvm得minor gc帮助释放;同时17默认参数就是hascleaner模式)
2)禁用了gc(如果不禁用, 就不会内存爆掉,cleaner模式申请内存会触发full gc,像1.1那样)
线上案例应该是1.2的场景
另一方面,Netty需要基于ByteBuf.release()方法执行其他操作,例如池化内存释放回内存池,否则该对象会被内存池一直标记为已使用
配置堆外内存大小的参数有-XX:MaxDirectMemorySize和-Dio.netty.maxDirectMemory,这2个参数有什么区别?
- -XX:MaxDirectMemorySize
用于限制Netty中hasCleaner策略的DirectByteBuffer堆外内存的大小,默认值是JVM能从操作系统申请的最大内存,如果内存本身没限制,则值为Long.MAX_VALUE个字节(默认值由Runtime.getRuntime().maxMemory()返回),代码位于java.nio.Bits#reserveMemory()方法中
note:-XX:MaxDirectMemorySize无法限制Netty中noCleaner策略的DirectByteBuffer堆外内存的大小(实践下来不是)
- -Dio.netty.maxDirectMemory
用于限制noCleaner策略下Netty的DirectByteBuffer分配的最大堆外内存的大小,如果该值为0,则使用hasCleaner策略,代码位于PlatformDependent#incrementMemoryCounter()方法中
- (1) hasCleaner的DirectByteBuffer监控
对于hasCleaner策略的DirectByteBuffer,java.nio.Bits类是有记录堆外内存的使用情况,但是该类是包级别的访问权限,不能直接获取,可以通过MXBean来获取
**note:**MXBean,Java提供的一系列用于监控统计的特殊Bean,通过不同类型的MXBean可以获取JVM进程的内存,线程、类加载信息等监控指标
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans();
BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0);
// hasCleaner的DirectBuffer的数量
long count = directBufferMXBean.getCount();
// hasCleaner的DirectBuffer的堆外内存占用大小,单位字节
long memoryUsed = directBufferMXBean.getMemoryUsed();
note: MappedByteBuffer:是基于FileChannelImpl.map进行进行mmap内存映射(零拷贝的一种实现)得到的另外一种堆外内存的ByteBuffer,可以通过ManagementFactoryHelper.getBufferPoolMXBeans().get(1)获取到该堆外内存的监控指标
- (2) noCleaner的DirectByteBuffer监控
Netty中noCleaner的DirectByteBuffer的监控比较简单,直接通过PlatformDependent.usedDirectMemory()访问即可
- disabled 完全关闭内存泄露检测
- simple 以约1%的抽样率检测是否泄露,默认级别
- advanced 抽样率同simple,但显示详细的泄露报告
- paranoid 抽样率为100%,显示报告信息同advanced
使用方法是在命令行参数设置:
-Dio.netty.leakDetectionLevel=[检测级别]
也可以通过jdk自带的Visualvm获取,需要安装Buffer Pools插件,底层原理是访问MXBean中的监控指标,只能获取hasCleaner的DirectByteBuffer的使用情况
Netty堆外内存泄漏的原因多种多样,例如代码漏了写调用release();通过retain()增加了ByteBuf的引用计数值而在调用release()时引用计数值未清空;因为Exception导致未能release();ByteBuf引用对象提前被GC,而关联的堆外内存未能回收等等,这里无法全部列举
exception:https://tech.meituan.com/2018/10/18/netty-direct-memory-screening.html
qps打爆:https://zhuanlan.zhihu.com/p/587822324?utm_id=0
0.2 https://www.cnblogs.com/stateis0/p/9062152.html
对于堆外内存,使用 System.gc() 是不靠谱的,依赖老年代 FGC 也是不靠谱的,而且大部分调优指南都设置了 -DisableExplicitGC 禁用 System.gc()。所以主动回收比较靠谱, JDK 在 DirectByteBuffer 中提供了 Cleaner 用来主动释放内存。同时还有 Unsafe 的 freeMemory 方法也可以。
每次申请直接内存都需要 Bits 判断是否足够,如果 FGC 后还不够,OOM,所以,这里的做法还是挺重要的)
注意:这个 Cleaner 是个虚引用,DirectByteBuffer 创建他的时候,会将自己放入虚引用的构造函数中,如果这个 DirectByteBuffer 被回收了(无人再引用这个 Cleaner),那么 GC 将会把这个 Cleaner 赋值给 Reference 的 pending 变量中,专门有一条 ReferenceHandler 的线程会死循环执行 Reference 的 tryHandlePending 方法,这个方法会调用 pending 的 clean 方法,完成内存回收操作。
所以,你知道了吧,noCleaner 的构造方法是不能调用 cleaner 的 clean 方法的。只能使用 unSafe 的 freeMemory 方法。而这就是 Netty 默认的做法。
同时,noCleaner 的构造方法也没有向 Bits 申请内存的内容,在申请内存的时候,性能会比 hasCleaner 要好一点。关于 Bits 的设计,我觉得不够优雅。当内存不够了,就 System.gc(),却只休眠 100 毫秒。根本不够回收到堆外内存。
实际上,Cleaner 的作用除了更新一下 Bits 的一些属性,方便下次申请内存之外,别无作用。
我猜想 Netty 使用 noCleaner 是性能优化的考虑吧。为了防止用户忘记使用 ReferenceCountUtil.release(), 导致内存泄漏,Netty 还使用了虚引用跟踪每一个 ByteBuf,基本上避免了内存泄漏的发生。
综上所述:noCleaner 无论是在申请内存还是释放内存都比使用 hasCleaner 性能好要好一点。
1
在hasCleaner模式下,没有release并不会真正导致泄漏,证据就是java11 一直压并没有垮掉
因为:(https://zhuanlan.zhihu.com/p/654366685)
PlatformDependent.useDirectBufferNoCleaner () 在 jdk 高版本下默认值是 false。那么每次申请直接内存都是通过 ByteBuffer.allocateDirect 来创建。那么到这个时候就已经定位到相关根因了,通过 ByteBuffer.allocateDirect 来申请直接内存,
如果内存不足的时候会强制系统 System.Gc (),并且会同步等待 DirectByteBuffer 通过 Cleaner 的虚引用回收内存。下面是 ByteBuffer.allocateDirect 预占内存(reserveMemory)的关键代码。
大概逻辑是 触达申请的最大的直接内存 -> 判断是否有相关的对象在 gc 回收 -> 没有在回收则主动触发 System.gc () 来触发回收 -> 在同步循环最多等待 MAX_SLEEPS 次数看是否有足够的直接内存。整个同步等待逻辑在亲测在 jdk17 版本最多能 1 秒以上。
没有release在hascleaner只会导致direct buffer要等到堆里面得对象回收才会clean,而还没等到堆回收,直接内存就爆了;它不是会system.gc()? 肯定被禁用了
证据:
1.1 未禁用时:仅-XX:MaxDirectMemorySize=100k

可以看到每20s一次莫名其妙得gc,1秒一个请求,请求body大小大约6k
100/6=18s
1.2 禁用:-XX:MaxDirectMemorySize=100k -XX:+DisableExplicitGC


25次挂掉(试了第二次是24),看来5:35得minor gc给它续了点命
1.3 再看看用nocleaner
-XX:MaxDirectMemorySize=100k -Dio.netty.tryReflectionSetAccessible=true


非常精准,多次尝试都是14次挂掉
2 小结及线上环境
| nocleaner | cleaner | |
| 禁用 | 14次挂 | 25次挂(有一些minor gc给它续命) |
| 不禁用 | 一直跑 |
所以线上究竟是啥环境,
写一个接口
System.gc()
PlatformDependent.usedDirectMemory()
结果:

结果为
hascleaner + has gc()
被xxx言中,因为漏了release,所以直接内存没有及时回收;同时cleaner 遇到阻力gc时,堆里面的东西somehow还在引用导致引用的直接内存无法回收
关键变成了 堆里那个对象的生命周期
https://www.jianshu.com/p/44d1a532a038 这篇文章有对netty http生命周期的探讨
3 hascleaner + no gc() + release fix
-XX:MaxDirectMemorySize=100k -XX:+DisableExplicitGC
实验结果没有oom
4 研究路径小结
对mock的html,mac 1.8 压垮了,为什么win java 11没压垮
证明了1.8下限制100k,6k的httpbody,不释放下多次14次挂 - 使用了nocleaner
证明了11下限制100k,不释放,同样的操作老是不挂 - 使用了cleaner+explict gc —— 这个案例与线上展现出差异,没有办法解释,这个组合按理能一直跑,有可能跟池有关
证明了11下disable explict gc,限制100k,不释放,has cleaner24或25次挂;nocleaner多次14次挂
证明了11下disable explict gc,限制100k,释放,不挂
两个问题:为什么uat压不垮,为什么prod一周垮了?
证明了线上环境允许explict gc,所以估计流量和并发量上来才挂,uat单url单线程还等1秒gap,jvm中那个http bytebuf在堆中的引用没有积压
怎么把uat压垮从而获得完整的BC AC压力测试报告
不弄了,https://www.jianshu.com/p/44d1a532a038有很好的追踪使用c++
浙公网安备 33010602011771号