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在启动时需要判断检查当前环境、环境配置参数是否允许noCleaner策略(具体逻辑位于PlatformDependent的static代码块),例如运行在Android下时,是没有Unsafe类的,不允许使用noCleaner策略,如果不允许,则使用hasCleaner策略

读到这里,也许有读者会问,如果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()

结果:

 

image

jstat -gc 18276 2000

结果为

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++ 

posted on 2024-04-15 16:40  silyvin  阅读(98)  评论(0)    收藏  举报