写点什么

首次排查 OOM 实录

用户头像
AI乔治
关注
发布于: 2020 年 11 月 21 日
首次排查 OOM 实录

前言

本篇文章的落成更像是一篇笔记,而不是博客。因为在一年的工作后,首次碰上了 OOM 问题,虽然导致的原因比较简单,但也算是值得纪念的,哈哈。

问题复现

代码源码:https://github.com/jitwxs/disruptor-study/blob/master/disruptor-demo/src/test/java/jit/wxs/disruptor/demo/oom

问题原因和 Disruptor 相关,如果不了解的同学,就把它理解成一个首尾相连的环形 Queue 就 OK 了。

代码实现

首先创建 Disruptor 存放的实体类 Entity,它有个对象叫 dataList,存放的是 EntityData 的引用:

@Datapublic class Entity {    private long id;
private List<EntityData> dataList;}
@Data@NoArgsConstructor(access = AccessLevel.PRIVATE)@AllArgsConstructorpublic class EntityData { private long id;
private String message;}
复制代码

创建 Disruptor 消费者,从队列中消费数据,打印了当前消费的 sequence,以及 dataList 中引用的对象数量:

class EntityEventHandler implements EventHandler<Entity> {
@Override public void onEvent(Entity event, long sequence, boolean endOfBatch) throws Exception { // 从 ringBuffer 中消费数据 System.out.println("EntityEventHandler Sequence: " + sequence + ", subList size: " + event.getDataList().size()); }}
复制代码

创建 Disruptor 生产者,通过调用 publish() 方法将数据放入队列中:

class EntityEventTranslator {    private final RingBuffer<Entity> ringBuffer;
public EntityEventTranslator(RingBuffer<Entity> ringBuffer) { this.ringBuffer = ringBuffer; }
private static final EventTranslatorTwoArg<Entity, Long, List<EntityData>> TRANSLATOR = (event, sequence, id, dataList) -> { event.setId(id); event.setDataList(dataList); };
public void publish(Long id, List<EntityData> dataList) { ringBuffer.publishEvent(TRANSLATOR, id, dataList); }}
复制代码

创建运行的主类,主要的业务操作就是一个死循环去生产数据。

public class OOMTest {    private static int BUFFER_SIZE = 65536;
public static void main(String[] args) throws InterruptedException { Disruptor<Entity> disruptor = new Disruptor<>(Entity::new, BUFFER_SIZE, DaemonThreadFactory.INSTANCE, ProducerType.SINGLE, new BlockingWaitStrategy());
// 2. 添加消费者 disruptor.handleEventsWith(new EntityEventHandler());
// 3. 启动 Disruptor RingBuffer<Entity> ringBuffer = disruptor.start();
// 4. 创建生产者 EntityEventTranslator producer = new EntityEventTranslator(ringBuffer);
// 5. 死循环发送事件 while (true) { long id = RandomUtils.nextLong(1, 100000); List<EntityData> dataList = mockData(RandomUtils.nextInt(10, 1000));
producer.publish(id, dataList);
TimeUnit.MILLISECONDS.sleep(10); } }
private static List<EntityData> mockData(int size) { List<EntityData> result = Lists.newArrayListWithCapacity(size); for(int i = 0; i < size; i++) { result.add(new EntityData(RandomUtils.nextLong(100000, 1000000), RandomStringUtils.randomAlphabetic(1, 100))); } return result; }}
复制代码

Java VisualVM

Java VisualVM 是自 JDK 1.6 起,安装时自带的 JVM 监控工具,本次我们使用它来监控程序运行后的堆内存情况。运行程序,启动 Java VisualVM 并连接上程序。可以观察到,随着程序的不断运行,堆的大小不断扩大。



复现 OOM

下面通过控制 JVM 的堆大小,来达到 OOM 的效果。在 IDEA 中配置运行主类的启动参数如下:

-Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\disruptor_oom.hprof
复制代码

第一个指定了堆的初始大小,第二个指定了堆的最大大小,第三个指定了当 JVM 发生 OOM 时,自动生成 dump 文件,第四个指定了 dump 文件的位置。



重新运行程序,由于我们配置了 Xmx,因此当内存增长到阈值时,触发 OOM,dump 文件生成在指定位置。



Heap Dump

Dump

Eclipse 的 MemoryAnalyzer 是目前最为常用的 dump 文件分析工具,有 Ecplise 插件版和独立版两种。由于我们使用的 IDE 是 IDEA,因此安装独立版即可。

安装完毕后,点击 File -> Open Heap Dump 打开生成的 dump 文件,等待右下角加载完毕后,选择 Leak Suspects Report 打开分析报告。




由于使用的这个 Case 没有干扰项,所以报告刚打开,Override 页其实就已经展示出了问题:

com.lmax.disruptor.RingBuffer @ 0xf8153f78Shallow Size: 144 B Retained Size: 232.6 MB
复制代码

我们假装看不见,点击 Leak Suspects,查看内存泄露分析报告。如图中 ① 所示,报告只出现了一个问题。实际项目这里可能会展示多个,你需要去找到真正导致 OOM 的那一个问题。



在 ② 处和 ③ 处,重点关注 Shallow Heap 和 Retained Heap 两个字段。专业的解释比较晦涩难懂,用大白话来说就是: Shallow Heap 表示对象自身的占用大小;Retained Heap 表示对象自身及其 GC 后可被释放的所有引用对象的大小。

以下面代码为例,Shallow Heap 表示 A 自身的大小,Retained Heap 表示 A + B 的大小(严谨的说,只有 B 不被其他对象所引用,即 GC 在释放掉 A 时也能释放掉 B 的话,Retained Heap 才为二者和)。

class A { B b; }
class B { String sss; }
复制代码

回到报告,在 ② 处说明了 RingBuffer 队列底层的 entries 数组,其 Shallow Heap 只有 144 byte,而 Retained Heap 有 243879304 byte,说明其引用了大量可以被 GC 的对象。在 ③ 处展示 entries 数组中每一项元素,也可以看出来。

问题原因

问题的原因就是这个 entries 数组太大了,大的原因是 Entity 对象有个 EntityData 的 dataList,每个 Entity 都持有了许多个 EntityData,导致 entries 数组较大。

虽然这些 Entity 对象及其持有的 EntityData 都是可以被 GC 的,但是不幸的是还没等到 GC 就已经 OOM 了。因此想要解决这个问题,就得让程序能够撑到 GC。

将程序主类中的 BUFFER_SIZE 从 65536 下调到 128,重新运行再试试看。



可以看到程序内存保持稳定。

BUFFER_SIZE 设置的就是这个 entries 数组的长度,前面说过可以把它理解成一个首尾相连的环形 Queue,那么当 entries 数组满了,又从头开始覆盖。当被覆盖后,原对象就失去了引用,就可以被 GC。

结语

生成 dump 文件的方式一般有两种,要么是在程序启动时,通过 JVM 参数,在出现 OOM 时自动 dump,就如同 2.3 节那样;另一种方式是通过命令,把当前的程序进行 dump:

jmap -dump:[live,]format=b,file=fileName [pid]
复制代码

jmap 也是在安装 JDK 时自动安装的小工具,可以帮助我们对堆进行 dump。live 参数是可选的,选中后只输出活跃的对象到 dunmp 文件,最后的 pid 指定 java 程序运行的进程号。例如:

jmap -dump:format=b,file=/home/admin/disruptor_oom.hprof 1235
复制代码

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:



  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java 烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章 ing🚀


出处:https://club.perfma.com/article/2002880

本文作者:Jitwxs


用户头像

AI乔治

关注

分享后端技术干货。公众号【 Java烂猪皮】 2019.06.30 加入

一名默默无闻的扫地僧!

评论

发布
暂无评论
首次排查 OOM 实录