JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

 

笔者是汽车之家实时计算平台的一名小伙伴。负责flink平台,数据湖及kafka平台的设计与开发。平时擅长做平台设计,定位及解决各种疑难杂症。第二篇文章,讲的点依旧很小,但是这次图多!!! 在这里感谢支持上篇文章的小伙伴了

前言

这篇文章是之前解决一个Flink任务在线上发生fullgc

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

 

当时的想法就是fullgc发生在:

  • 堆内存: 通过堆内存监控和dump堆内存这两种方式 都发现堆占用不大 ,排除

  • metaspace: gc日志里并没有堆metaspace日志且metaspace占用很小 ,排除

因此主要把排除重点放在直接内存。接下来老规矩,我们长话短说会夹带一点点心路历程。

 

步骤

1.配置jvm参数打印gc

 clusterConf.env.java.opts=-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${LOG_DIRS}/gc.log  

 

2.在gc日志里发现有程序主动调用System.gc()

 

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

 

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

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

加了些日志,发现并没有调用。

3.使用arthas

3.1.下载 arthas,并attach需要监听的jvm进程
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

 

 

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

3.2.监听 System.gc()方法并把日志打到磁盘上
options unsafe true

stack java.lang.System gc -n 1 >> /tmp/gc.log &

 

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

3.3 观察日志

过了一段时间 观察下日志发现

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

是在Kafka connector 调用java的nio的时候调用的System.gc()

4.分析

4.1讲一下这块代码的大致背景

java的直接内存(direct memory)是不会被gc回收的,而是通过监听持有直接内存资源的对象被回收的时候才把直接内存释放掉的。主要原理使用到虚引用和引用队列:当jvm发现对象已经没有强引用,仅剩虚引用时会将其存放在Reference的discovered的列表中 --->随后变成Pending状态 (准备加入引动队列) ---->随后进入Enqueued状态(加入队列,监听者可以获取从而回收资源)

 

4.2背景介绍完毕,回到刚才找到的方法栈,看倒数第二个方法

 

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

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

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

DirectByteBuffer 在构造时便被Cleaner监听回收资源。束于篇幅稍微文字介绍下Cleaner, Cleaner 就是 PhantomReference(虚引用)的子类。 1.Cleaner#create的方法就是将DirectByteBuffer对象虚引用且监听引用队列,2.当在队列中接收到DirectByteBuffer(此时对象已经被jvm标记discovered) 执行Deallocator的回收资源操作。

 

4.3 倒数第一个方法

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

 

JVM诊断及工具笔记(2)使用arthas定位哪里执行了System#gc()

来到倒数第一个方法,打开实现大家应该就能明白,这个方法是给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了。

 

上一篇:arthas使用总结


下一篇:网易二面-Arthas的底层原理