这篇文章依旧讲得很短,不过图挺多,如果图加载不出来辛苦多刷新两次,图床不是很给力。
前言
这篇文章记录了一个线上Flink任务发生fullgc的定位过程。

当时的想法就是fullgc发生在:
堆内存: 通过堆内存监控和fullgc的时候dump堆内存这两种方式发现堆占用不大 ,可排除
metaspace: gc日志里并没有metaspace oom日志且metaspace占用很小 ,可排除
因此主要把排除重点放在直接内存。接下来老规矩,我们长话短说稍微夹带一点点心路历程。
步骤
1.配置jvm参数打印gc
clusterConf.env.java.opts=-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${LOG_DIRS}/gc.log
2.在gc日志里发现有程序主动调用System.gc()

当时猜测是使用的flink11版本代码里面会有调用

加了些日志,发现并没有调用。
3.使用arthas
3.1.下载 arthas,并attach需要监听的jvm进程
curl -O https://arthas.aliyun.com/arthas-boot.jarjava -jar arthas-boot.jar

3.2.监听 System.gc()方法并把日志打到磁盘上
options unsafe truestack java.lang.System gc -n 1 >> /tmp/gc.log &

3.3 观察日志
过了一段时间 观察下日志发现是在Kafka connector 调用java的nio的时候调用的System.gc()。

4.分析
4.1讲一下这个方法栈的大致背景

java的直接内存(direct memory)是不会被gc回收的。而是在监听到持持有直接内存资源的对象被gc标记的时候,才将其持有的资源回收。主要原理使用到虚引用和引用队列。即当jvm发现对象已经没有被强引用仅剩虚引用时,会将其存放在Reference的discovered的列表中 --->随后变成Pending状态 (准备加入引用队列) ---->随后进入Enqueued状态(加入队列,从而监听者回收资源)
4.2背景介绍完毕,看方法栈倒数第二个方法

栈里 DirectByteBuffer 这个类就是直接内存资源的持有者。

DirectByteBuffer在构造时便被Cleaner监听回收资源。限于篇幅稍微文字介绍下Cleaner, Cleaner 就是 PhantomReference(虚引用)的子类。1.Cleaner#create方法作用就是将DirectByteBuffer对象虚引用且监听引用队列,2.当在队列中接收到DirectByteBuffer(此时对象的状态是Enqueued) 执行Deallocator的回收资源操作。
4.3 倒数第一个方法


来到倒数第一个方法,打开实现大家应该就能明白,这个方法是给DirectByteBuffer预留资源的。如果直接内存充足,万事大吉。如果直接内存不充足: 大家回忆下刚才讲的被虚引用DirectByteBuffer的discovered,Pending,Enqueued状态, 图中我用箭头标记的jlra.tryHandlePendingReference() 是尽快将pending状态的对象尽快转换成enqueued状态好被Cleaner回收掉。一顿操作过后发现直接内存依旧不够。那么只能调用System.gc() 方法执行fullgc了。
注: 部分场景使用fullgc是有必要的,有部分DirectByteBuffer对象很多次回收不掉进入老年代之后,这个时候young gc是没有办法回收掉的。
分析那么多,其实总结起来就一句话:直接内存不够了。
5.总结
回头又看了下用户任务的特点。发现数据消费量很大,但是代码可用的直接内存不多,反倒是框架可用的直接内存(taskmanager.memory.framework.off-heap.size)被设得很大。合理调节完资源后,这个任务就再也没有发生oom了。




