精华内容
下载资源
问答
  • 2021-03-17 14:54:13

    记一次OOM问题排查过程

    发布时间:2019-05-01 20:05,

    浏览次数:459

    , 标签:

    OOM

    上周运维反馈线上程序出现了OOM,程序日志中的输出为

    Exception in thread "http-nio-8080-exec-1027" java.lang.OutOfMemoryError: Java

    heap space Exception in thread "http-nio-8080-exec-1031"

    java.lang.OutOfMemoryError: Java heap space

    看线程名称应该是tomcat的nio工作线程,线程在处理程序的时候因为无法在堆中分配更多内存出现了OOM,幸好JVM启动参数配置了-XX:+HeapDumpOnOutOfMemoryError,使用MAT打开拿到的hprof文件进行分析。

    第一步就是打开Histogram看看占用内存最大的是什么对象:

    可以看到byte数组占用了接近JVM配置的最大堆的大小也就是8GB,显然这是OOM的原因。

    第二步看一下究竟是哪些byte数组,数组是啥内容:

    可以看到很明显这和HTTP请求相关,一个数组大概是10M的大小。

    第三步通过查看GC根查看谁持有了数组的引用:

    这符合之前的猜测,是tomcat的线程在处理过程中分配了10M的buffer在堆上。至此,马上可以想到一定是什么参数设置的不合理导致了这种情况,一般而言tomcat不可能为每一个请求分配如此大的buffer。

    第四步就是检查代码里是否有tomcat或服务器相关配置,看到有这么一个配置:

    max-http-header-size: 10000000

    至此,基本已经确定了八九不离十就是这个不合理的最大http请求头参数导致的问题。

    到这里还有3个疑问:

    * 即使一个请求分配10M内存,堆有8GB,难道当时有这么多并发吗?800个tomcat线程?

    * 参数只是设置了最大请求头10M,为什么tomcat就会一次性分配这么大的buffer呢?

    * 为什么会有如此多的tomcat线程?感觉程序没这么多并发。

    先来看问题1,这个可以通过MAT在dump中继续寻找答案。

    可以打开线程视图,搜索一下tomcat的工作线程,发现线程数量的确很多有401个,但是也只是800的一半:

    再回到那些大数组的清单,按照堆分配大小排序,往下看:

    可以发现除了有10008192字节的数组还有10000000字节的数组,查看引用路径可以看到这个正好是10M的数组是output

    buffer,区别于之前看到的input buffer:

    好吧,这就对了,一个线程分配了输入输出两个buffer,占用20M内存,一共401个线程,占用8GB,所以OOM了。

    还引申出一个问题为啥有这么多工作线程,

    再来看看问题2,这就需要来找一下源码了,首先max-http-header-size是springboot定义的参数,查看springboot代码可以看到这个参数对于tomcat设置的是MaxHttpHeaderSize:

    然后来看看tomcat源码:

    进一步看一下input buffer:

    buffer大小是MaxHttpHeaderSize+ReadBuffer大小,这个默认是8192字节:

    (int)Each

    connection that is opened up in Tomcat get associated with a read ByteBuffer.

    This attribute controls the size of this buffer. By default this read buffer is

    sized at 8192 bytes. For lower concurrency, you can increase this

    to buffer more data. For an extreme amount of keep alive connections, decrease

    this number or increase your heap size.

    这也就是为什么之前看到大量的buffer是10008192字节的。至于为什么分配的buffer需要是MaxHttpHeaderSize+ReadBuffer。显然还有一批内容是空的10000000字节的buffer应该是output

    buffer,源码可以印证这点:

    嗯这是一个header buffer,所以正好是10000000字节。

    至于问题3,显然我们的应用程序是配置过最大线程的(查看配置后发现的确,我们配置为了2000,好吧有点大),否则也不会有401个工作线程(默认150),如果当时并发并不大的话就一种可能,请求很慢,虽然并发不大,但是因为请求执行的慢就需要更多线程,比如TPS是100,但是平均RT是4s的话,就是400线程了。这个问题的答案还是可以通过MAT去找,随便看几个线程可以发现很多线程都在等待一个外部服务的返回,这说明外部服务比较慢,去搜索当时的程序日志可以发现有很多"feign.RetryableException:

    Read timed out executing的日志"。。。。追杀下游去!慢点,我们的feign的timeout也需要再去设置一下,别被外部服务拖死了。

    更多相关内容
  • 主要给大家介绍了一次OOM问题排查过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
  • 一次OOM问题排查

    2018-01-18 17:39:16
    问题描述 用户问题:用户发现自己...经过我们的排查,发现cpu的两间歇飙高是由于客户系统当时发生了OOM(out of memory)的情况,并触发了oom-killer造成的。但客户并不接受这个结论,认为是云服务器的异常导致了c...

    问题描述

    用户问题:
    用户发现自己的服务器CPU在某一时刻陡然升高,但从监控上看,同一时刻的业务量却并不高,客户怀疑是云服务器有问题,希望技术支持团队予以解决。

    经过我们的排查,发现cpu的两次间歇飙高是由于客户系统当时发生了OOM(out of memory)的情况,并触发了oom-killer造成的。但客户并不接受这个结论,认为是云服务器的异常导致了cpu飙高,而cpu的升高又导致了oom情况的发生。也就是对于cpu升高和oom谁为因果这件事上,客户和我们持完全相反的态度。

    下面我们将通过对oom时系统日志的解读来说明cpu升高和oom之间的因果关系。

    知识点梳理

    1 预备知识

    在解读日志之前,我们先回顾一下linux内核的内存管理。

    1.1 几个基本的概念

    (1)Page 页
    处理器的最小‘寻址单元’是字节或者字,而页是内存的‘管理单元’。

    (2) Zone 区
    (a)区存在的原因:
    有些硬件设备只能对特定的内存地址执行DMA(direct memory access)操作。
    在一些架构中,实际物理内存是比系统可寻址的虚拟内存要大的,这就导致有些物理内存没有办法被永久的映射在内核的地址空间中。

    区的划分也是直接以上面两个原因为依据的。

    (b)区的种类
    ZONE_DMA—这个区包含的page可以执行DMA操作。这部分区域的大小和CPU架构有关,在x86架构中,该部分区域大小限制为16MB。
    ZONE_DMA32—类似于ZOME_DMA, 这个区也包含可以执行DMA操作的page。该区域只存在于64位系统中,适合32位的设备访问。
    ZONE_NORMAL—这个区包含可以正常映射到地址空间中的page,或者说这个区包含了除了DMA和HIGHMEM以外的内存。许多内核操作都仅在这个区域进行。
    ZONE_HIGHMEM—这个区包含的是high memory,也就是那些不能被永久映射到内核地址空间的页。

    32位的x86架构中存在三种内存区域,ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。根据地址空间划分的不同,三个区域的大小不一样:

    1)1G内核空间/3G用户空间

    ZONE_DMA< 16M
    ZONE_NORMAL16M~896M
    ZONE_HIGHMEM> 896M

    2) 4G内核空间/4G用户空间

    ZONE_DMA< 16M
    ZONE_NORMAL16M~3968M
    ZONE_HIGHMEM> 3968M

    64位的系统由于寻址能力的提高,不存在highmem区,所以64位系统中存在的区有DMA,DMA32和NORMAL三个区。

    ZONE_DMA< 16M
    ZONE_DMA3216M~4G
    ZONE_NORMAL> 4G

    1.2 内核分配内存的函数

    下面是内核分配内存的核心函数之一,它会分配2的order次方个连续的物理页内存,并将第一页的逻辑地址返回。

    unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

    内核空间的内存分配函数和用户空间最大的不同就是每个函数会有一个gfp_mask参数。
    其中gfp 代表的就是我们上面的内存分配函数 __get_free_pages()。

    gfp_mask可以分成三种: 行为修饰符(action modifier),区修饰符 (zone modifier)和类型( type).

    (1)行为修饰符是用来指定内核该如何分配内存的。比如分配内存时是否可以进行磁盘io,是否可以进行文件系统操作,内核是否可以睡眠(sleep)等等。
    (2)区修饰符指定内存需要从哪个区来分配。
    (3)类型是行为修饰符和区修饰符结合之后的产物。在一些特定的内存分配场合下,我们可能需要同时指定多个行为修饰符和区修饰符,而type就是针对这些固定的场合,将所需要的行为修饰符和区修饰符都整合到了一起,这样使用者只要指定一个type就可以了。

    不同type所代表的含义可以参看下面的表格:

    2 日志解读

    下面是从oom killer被触发到进程到被杀掉的一个大概过程,我们来具体看一下。

    nginx invoked oom-killer: gfp_mask=0x200da, order=0, oom_score_adj=0
    nginx cpuset=6011a7f12bac1c4592ce41407bb41d49836197001a0e355f5a1d9589e4001e42 mems_allowed=0
    CPU: 1 PID: 10242 Comm: nginx Not tainted 3.13.0-86-generic #130-Ubuntu
    Hardware name: Xen HVM domU, BIOS 4.0.1 12/16/2014
     0000000000000000 ffff880070611a00 ffffffff8172a3b4 ffff88012af6c800
     0000000000000000 ffff880070611a88 ffffffff8172495d ffffffff81069b76
     ffff880070611a60 ffffffff810ca5ac ffff88020fff7e38 0000000000000000
    Node 0 DMA free:15908kB min:128kB low:160kB high:192kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15992kB managed:15908kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
    lowmem_reserve[]: 0 3746 7968 7968
    Node 0 DMA32 free:48516kB min:31704kB low:39628kB high:47556kB active_anon:3619272kB inactive_anon:216kB active_file:556kB inactive_file:1516kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:3915776kB managed:3836724kB mlocked:0kB dirty:4kB writeback:0kB mapped:324kB shmem:1008kB slab_reclaimable:67136kB slab_unreclaimable:67488kB kernel_stack:1792kB pagetables:14540kB unstable:0kB bounce:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:7365 all_unreclaimable? yes
    lowmem_reserve[]: 0 0 4221 4221
    Node 0 Normal free:35640kB min:35748kB low:44684kB high:53620kB active_anon:4019124kB inactive_anon:292kB active_file:1292kB inactive_file:2972kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:4456448kB managed:4322984kB mlocked:0kB dirty:24kB writeback:4kB mapped:1296kB shmem:1324kB slab_reclaimable:81196kB slab_unreclaimable:83432kB kernel_stack:3392kB pagetables:20252kB unstable:0kB bounce:0kB free_cma:0kB writeback_tmp:0kB pages_scanned: 7874 all_unreclaimable? yes
    lowmem_reserve[]: 0 0 0 0
    Node 0 DMA: 1*4kB (U) 0*8kB 0*16kB 1*32kB (U) 2*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (R) 3*4096kB (M) = 15908kB Node 0 DMA32: 1101*4kB (UE) 745*8kB (UEM) 475*16kB (UEM) 263*32kB (EM) 88*64kB (UEM) 25*128kB (E)12*256kB (EM) 6*512kB (E) 7*1024kB (EM) 0*2048kB 0*4096kB = 48524kB
    Node 0 Normal: 5769*4kB (EM) 1495*8kB (EM) 24*16kB (UE) 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 35420kB
    Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
    2273 total pagecache pages
    0 pages in swap cache
    Swap cache stats: add 0, delete 0, find 0/0
    Free swap  = 0kB
    Total swap = 0kB
    2097054 pages RAM
    0 pages HighMem/MovableOnly
    33366 pages reserved
    [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
    [  355]     0   355     4868       66      13        0             0 upstart-udev-br
    [  361]     0   361    12881      145      28        0         -1000 systemd-udevd
    [  499]     0   499     3814       60      13        0             0 upstart-socket-
    [  562]     0   562     5855       79      15        0             0 rpcbind
    [  644]   106   644     5398      142      16        0             0 rpc.statd
    [  775]     0   775     3818       58      12        0             0 upstart-file-br
    ...(此处有省略)
    [10396]   104 10396    21140    12367      44        0             0 nginx
    [10397]   104 10397    21140    12324      44        0             0 nginx
    [10398]   104 10398    21140    12324      44        0             0 nginx
    [10399]   104 10399    21140    12367      44        0             0 nginx
    Out of memory: Kill process 10366 (nginx) score 6 or sacrifice child
    Killed process 10366 (nginx) total-vm:84784kB, anon-rss:49156kB, file-rss:520kB
    

    先来看一下第一行,它给出了oom killer是由谁触发的信息。

    nginx invoked oom-killer: gfp_mask=0x200da, order=0, oom_score_adj=0

    order=0 告诉我们所请求的内存的大小是多少,即nginx请求了2的0次方这么多个page的内存,也就是一个page,或者说是4KB。

    gfp_mask的最后两个bit代表的是zone mask,也就是说它指明内存应该从哪个区来分配。

    Flag value Description

                0x00u      0 implicitly means allocate from ZONE_NORMAL

    __GFP_DMA 0x01u Allocate from ZONE_DMA if possible
    __GFP_HIGHMEM 0x02u Allocate from ZONE_HIGHMEM if possible

    (这里有一点需要注意,在64位的x86系统中,是没有highmem区的,64位系统中的normal区就对应上表中的highmem区。)

    在本案例中,zonemask是2,也就是说nginx正在从zone-normal(64位系统)中请求内存。

    其他标志位的含义如下:

    #define __GFP_WAIT      0x10u   /* Can wait and reschedule? */
    #define __GFP_HIGH      0x20u   /* Should access emergency pools? */
    #define __GFP_IO        0x40u   /* Can start physical IO? */
    #define __GFP_FS        0x80u   /* Can call down to low-level FS? */
    #define __GFP_COLD      0x100u  /* Cache-cold page required */
    #define __GFP_NOWARN    0x200u  /* Suppress page allocation failure warning */
    #define __GFP_REPEAT    0x400u  /* Retry the allocation.  Might fail */
    #define __GFP_NOFAIL    0x800u  /* Retry for ever.  Cannot fail */
    #define __GFP_NORETRY   0x1000u /* Do not retry.  Might fail */
    #define __GFP_NO_GROW   0x2000u /* Slab internal usage */
    #define __GFP_COMP      0x4000u /* Add compound page metadata */
    #define __GFP_ZERO      0x8000u /* Return zeroed page on success */
    #define __GFP_NOMEMALLOC 0x10000u /* Don't use emergency reserves */
    #define __GFP_NORECLAIM  0x20000u /* No realy zone reclaim during allocation */
    

    所以我们当前这个内存请求带有这几个标志:GFP_NORECLAIM,GFP_FS,GFP_IO,GFP_WAIT, 都是比较正常的几个标志,那么我们这个请求为什么会有问题呢?继续往下看,可以看到下面的信息:

    Node 0 Normal free:35640kB min:35748kB low:44684kB high:53620kB active_anon:4019124kB inactive_anon:292kB active_file:1292kB inactive_file:2972kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:4456448kB managed:4322984kB mlocked:0kB dirty:24kB writeback:4kB mapped:1296kB shmem:1324kB slab_reclaimable:81196kB slab_unreclaimable:83432kB kernel_stack:3392kB pagetables:20252kB unstable:0kB bounce:0kB free_cma:0kB writeback_tmp:0kB pages_scanned: 7874 all_unreclaimable? yes
    

    可以看到normal区free的内存只有35640KB,比系统允许的最小值(min)还要低,这意味着application已经无法再从系统中申请到内存了,并且系统会开始启动oom killer来缓解系统内存压力。

    这里我们说一下一个常见的误区,就是有人会认为触发了oom-killer的进程就是问题的罪魁祸首,比如我们这个例子中的这个nginx进程。其实日志中invoke oom-killer的这个进程有时候可能只是一个受害者,因为其他应用/进程已将系统内存用尽,而这个invoke oomkiller的进程恰好在此时发起了一个分配内存的请求而已。在系统内存已经不足的情况下,任何一个内存请求都可能触发oom killer的启动。

    oom-killer的启动会使系统从用户空间转换到内核空间。内核会在短时间内进行大量的工作,比如计算每个进程的oom分值,从而筛选出最适合杀掉的进程。我们从日志中也可以看到这一筛选过程:

    [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
    [  355]     0   355     4868       66      13        0             0 upstart-udev-br
    [  361]     0   361    12881      145      28        0         -1000 systemd-udevd
    [  499]     0   499     3814       60      13        0             0 upstart-socket-
    [  562]     0   562     5855       79      15        0             0 rpcbind
    [  644]   106   644     5398      142      16        0             0 rpc.statd
    [  775]     0   775     3818       58      12        0             0 upstart-file-br
    ...
    [10396]   104 10396    21140    12367      44        0             0 nginx
    [10397]   104 10397    21140    12324      44        0             0 nginx
    [10398]   104 10398    21140    12324      44        0             0 nginx
    [10399]   104 10399    21140    12367      44        0             0 nginx
    

    本例中,一个nginx进程被选中作为缓解内存压力的牺牲进程:
    Out of memory: Kill process 10366 (nginx) score 6 or sacrifice child
    Killed process 10366 (nginx) total-vm:84784kB, anon-rss:49156kB, file-rss:520kB

    整个过程进行的时间很短,只有毫秒级别,但是工作量/计算量很大,这就导致了cpu短时间内迅速飙升,出现峰值。但这一切工作都由内核在内核空间中完成,所以用户在自己的业务监控数据上并不会看到业务量的异常。这些短时间升高的cpu是内核使用的,而不是用户的业务。

    本例中客户只是偶尔看到这个现象,且业务并没有受到影响。我们给客户的建议是分析业务内存需求量最大值,如果系统已经没有办法满足特定时段业务的内存需求,建议用户升级内存来避免oom的情况发生,因为严重的oom情况是可能引发系统崩溃的。

    展开全文
  • 作者:jitwxshttps://jitwxs.cn/f4adeb1d.html、前言本篇文章的落成更像是篇笔记,而不是博客。因为在一年的工作后,首次碰上了 OOM 问题,虽然导致的...

    作者:jitwxs

    https://jitwxs.cn/f4adeb1d.html

    一、前言

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

    二、问题复现

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

    2.1 代码实现

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

    @Data
    public class Entity {
        private long id;
    
        private List<EntityData> dataList;
    }
    
    @Data
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    @AllArgsConstructor
    public 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;
        }
    }
    

    2.2 Java VisualVM

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

    2.3 复现 OOM

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

    -Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\disruptor_oom.hprof
    

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

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

    三、Heap Dump

    3.1 分析 Dump

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

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

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

    com.lmax.disruptor.RingBuffer @ 0xf8153f78
    Shallow 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 数组中每一项元素,也可以看出来。

    3.2 问题原因

    问题的原因就是这个 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
    

    另外本文使用到的 Java VisualVM 和 MemoryAnalyzer 这两款工具,并没有详细介绍其功能,后续我将专门辟文,去介绍 Java 性能分析中常用的工具。

    推荐好文

    强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!
    
    分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
    能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!
    
    
    
    
    展开全文
  • 一次线上OOM问题排查

    千次阅读 2019-08-05 21:29:22
    1、出现OOM问题了,脑袋里第反应就是项目中出现内存泄露或者内存溢出了。先登录ELK,根据关键词 “java.lang.OutOfMemoryError” 进行搜索,果然发现有OOM错误日志。 org.springframework.scheduling.q...

    一、短信预警
    某天下午风和日丽,下午五点钟早早的就发版上线。七点准备下班的时候突然收到短信预警,项目OOM了,wtf !!!

    二、问题排查
    1、出现OOM问题了,脑袋里第一反应就是项目中出现内存泄露或者内存溢出了。先登录ELK,根据关键词 “java.lang.OutOfMemoryError” 进行搜索,果然发现有OOM错误日志。

    org.springframework.scheduling.quartz.JobMethodInvocationFailedException: Invocation of method 'run' on target class [class com.kingdee.finance.data.credit.ydreport.task.YdReportAppendixInfoTask] failed; nested exception is java.lang.OutOfMemoryError: Java heap space
        at org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob.executeInternal(MethodInvokingJobDetailFactoryBean.java:266)
        at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:75)
        at org.quartz.core.JobRunShell.run(JobRunShell.java:213)
        at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:557)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:3332)
        at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
        at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:622)
        at java.lang.StringBuffer.append(StringBuffer.java:383)
        at java.net.URLEncoder.encode(URLEncoder.java:271)
        at com.longsec.http.HttpConnection.httpPostWithJSON(HttpConnection.java:548)
        at com.kingdee.finance.data.credit.ydreport.service.impl.YdCreditAppendixInfoHandler.sendRequest(YdCreditAppendixInfoHandler.java:151)
        at com.kingdee.finance.data.credit.ydreport.service.impl.YdCreditAppendixInfoHandler.handleAppendixRequest(YdCreditAppendixInfoHandler.java:46)
        at com.kingdee.finance.data.credit.ydreport.service.impl.YdCreditAppendixInfoHandler$$FastClassBySpringCGLIB$$a4a2cc16.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671)
        at com.kingdee.finance.data.credit.ydreport.service.impl.YdCreditAppendixInfoHandler$$EnhancerBySpringCGLIB$$e0cc861a.handleAppendixRequest(<generated>)
        at com.kingdee.finance.data.credit.ydreport.task.YdReportAppendixInfoTask.handleAppendixInfoTask(YdReportAppendixInfoTask.java:56)
        at com.kingdee.finance.data.credit.ydreport.task.YdReportAppendixInfoTask.run(YdReportAppendixInfoTask.java:43)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:265)
        at org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob.executeInternal(MethodInvokingJobDetailFactoryBean.java:257)
        ... 3 more
    

    2、根据OOM堆栈错误日志,去扫了一眼代码并未看出什么问题。既然代码没有明显的内存泄露迹象,只能把目光转向堆内存对象分析了。

    3、从服务器上导出堆内存溢出的dump文件,用mat 打开分析一波。
    mat打开dump文件
    可以看到一个StringBuffer占了400M,另一个String对象占了224M,堆内存溢出跟这两个对象脱不了干系。搜索了下代码发现项目中出现问题的地方并没有用到StringBuffer这个类,应该是某个jar里有引用到的。点击String对象,看看value。
    查看String的Value

    4、根据String 对象的值,推测这是一个json格式的参数。用“username”和“businessNumber” 在代码中搜索。
    代码1
    这段代码就是组装请求参数,上面224M的String对象就是由这段代码产生。但出现400M的StringBuffer对象才是导致OOM的元凶,需要继续追查。
    代码2
    代码3
    代码4
    代码5
    整理出代码的调用关系,packRequestJson —> sendRequest —> HttpConnection.httpPostWithJSON —> URLEncoder.encode。很显然StringBuffer对象是由jar包中的URLEncoder.encode 方法中产生,这也跟日志中的堆栈日志正好也能对应上。
    日志

    三、问题原因
    根据上面分析以及查看代码,得知定时任务一次拉取 300条记录批次处理,组装参数得到一个224M 大小的String对象,然后在HTTP请求发送之前经过 URLEncoder.encode 方法 产生了一个400M大小的StringBuffer,直接导致了“java.lang.OutOfMemoryError” 异常。

    四、解决方案
    400M的大对象是由于一次取出300条记录,参数拼接生成的。可以将批处理记录数调小一点,就可以解决问题,批处理记录数应从数据配置中获取。

    四、总结
    1、线上异常预警,首先要查看项目日志。根据错误日志,来判断问题出现原因。确定好问题排查方向再选择相应的工具进行分析,多角度的观察,抽丝剥茧,直到找到问题的根源。

    2、问题排查要熟悉Java应用排查问题常用工具,比如:jstack、jmap、jps。
    查看某个应用的进程id以及启动参数:jps -v | grep “xxx”
    dump堆内存:jmap -dump:live,format=b,file=/heapdump.hprof {pid}
    导出线程栈:jstack {pid} > /jstack.txt

    展开全文
  • OOM简介 以下为转载的一些知识点,这边主要是记录以下排查过程 1.jvm内存分配不够,电脑内存的大小,不等于java程序能够使用的内存大小。jvm分配的内存大小,可以在JVM启动时,通过配置文件配置。 2.内存利用...
  • 一次线上OOM 问题排查过程!

    千次阅读 2019-09-08 11:00:00
    公众号后台回复“学习”,获取作者独家秘制精品资料扫描下方海报二维码,试听课程:上周运维反馈线上程序出现了OOM,程序日志中的输出为Exception inthread ...
  • 一次oom问题排查

    2022-01-17 08:50:45
    今天给大家分享最近出现的OOM问题。 上周五早上,测试同学反馈测试环境的子系统服务一直超时,请求没有响应。 收到这个问题之后,我有点纳闷,最近这个系统也没有改动代码逻辑,怎么会突然报服务超时的问题。为避免...
  • OOM排查思路

    千次阅读 2021-05-19 17:35:09
    oom的原因其实就个,内存不够了,我们要细分的就是哪里的内存不够了,以及为什么内存不够了 栈溢出 这种情况属实比较少,一般就是栈调用太深了,比如有人写了超长超长的递归方法。 方法区溢出 方法区存的都是类...
  • Java线上环境OOM问题排查

    千次阅读 2022-02-28 22:27:25
    这次跟大家分享的是如何解决线上环境OOM问题
  • Java OOM问题如何排查

    2021-11-08 17:22:24
    Java OOM问题如何排查 - 南山饱虎 - 博客园
  • 测试同学在管理后台上传一张3M的图片时,发现界面停留在卡死的状态,就喊我排查下。听到这个后首先要做的就是看这个管理后台服务的日志。 从日志里清楚的看到,居然是可恶的堆内存溢出。...MAT是款强大的
  • 一次 Java 进程 OOM排查分析(glibc 篇)
  • OOM排查 以上紧急处理方法只是治标不治本,为防止该问题以后再发生,需要搞清楚为什么会包OOM。 日志分析 通过错误日志定位到具体代码位置,以及疑似与字符串拼接有关,下图是问题代码: 找到问题代码,发现是对个...
  • 项目出现oom,记录一次使用dump排查问题,以及排查的思路
  • 案例实战:一次服务类加载器过多引发的OOM问题排查实践!欢迎大家加入我们的儒猿技术交流群,一个纯粹的交流技术、分享面经的地方。群里有一线大厂助教答疑、专栏优秀作业交流、互联网大厂面经分享、知名互联网公司...
  • 一次生产OOM问题排查

    2020-05-23 17:48:39
    我们有个生产服务,规模是12台机器*6个节点 = 72个节点的服务,最近老是出现某个节点突然挂掉的情况,问题出现频繁,天需要重启很多个节点 查看tomcat日志,发现是堆内存溢出 使用jmap -heap pid查看各个JVM内存...
  • OOM问题排查及原因解析

    千次阅读 2019-11-06 16:31:11
    、前言 最近公司线上出了故障,有业务反馈说线上某台机器发出的请求status都是101,代表是超时。于是顺着调用栈和监控去查,最后发现这台机器上的网关挂掉了,所以导致请求发不出去,导致业务超时。那为啥...
  • 在很久之前的项目上线出现了一次OOM,当时凭着jmap和VisualVM。定位了问题并解决问题。但是因为时间久远,怕以后忘记定位这种堆内存溢出的定位步骤,所以今天自己造了一个OOM,来复习并记录定位步骤。 首先制造OOM...
  • 考虑到QA刚刚在做压测,想到是不是出现了OOM问题。搜索错误日志: cat error.log | grep "OutOf" 果然有OOM错误日志: java.lang.OutOfMemoryError: GC overhead limit exceeded java.lang.OutOfMemoryError: GC ...
  • 容器OOM问题排查思路

    千次阅读 2018-11-26 21:52:19
    序言 又是个冬季,在这寒冷的冬季,总是让人心动。。。迷雾之城 外界的刁难,挑战。。。其实并不是最难的,最难的总是内部难以安抚,OOM。。。内存泄漏,OOM ...
  • jvm问题排查,使用jprofiler分析dump
  • 经过我们的排查,发现cpu的两间歇飙高是由于客户系统当时发生了OOM(out of memory)的情况,并触发了oom-killer造成的。但客户并不接受这个结论,认为是云服务器的异常导致了cpu飙高,而cpu的升高又导致了oom情况的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,904
精华内容 4,361
关键字:

一次oom问题排查