天天看点

千丝万缕的FGC与Buffer pool

1 背景

运维通知,线上系统一直在fgc,通过zabbix查看gc 的次数

千丝万缕的FGC与Buffer pool

再查看ygc和fgc空间占用情况

千丝万缕的FGC与Buffer pool

这里有几个疑问:

1:old space 空间一直很低,为什么会有频繁的fgc?

2:eden space 回收的阈值为什么越来越低,越来越频繁?

3:从eden space空间看一直在ygc,但是从ygc的次数看并没有过ygc?

4:fgc的越来越频繁,到最后为什么一直在fgc?

第一个问题

通过查看打印出来的error日志,确定是direct buffer 不够。在申请directbytebuffer的时候,会检查是否还有空闲的空间,剩余空间不够,则会调用system.gc,引起fgc(具体后续会详细介绍)。这里可以解释old space很低,但是一直fgc。并不是old区不够用,而是堆外空间不够用。

千丝万缕的FGC与Buffer pool

第二个问题

fgc是对整个堆进行gc(包含eden space,old space,perm space),fgc越来越频繁,会导致eden space 回收的越来越频繁。 正常的ygc触发是新建对象,申请不到eden space空间,才会进行ygc,但是这里是fgc引起的ygc,所以并不是eden space满了才会进行ygc。

第三个问题

统计ygc的次数一般是通过jmx或者日志方式统计。

日志方式:统计ygc日志出现的次数。下面为ygc的日志。

[gc2016-11-26t00:05:50.539+0800: 36.542: [parnew: 3355520k->34972k(3774912k), 0.0249160 secs] 3355520k->34972k(7969216k), 0.0250290 secs] [times: user=0.31 sys=0.00, real=0.03 secs]

fgc引起的eden space 回收没有打印ygc日志或者 collectioncount的增加。

第四个问题

申请directbytebuffer,会新建cleaner对象(负责资源的回收)。在gc时,会释放没有引用的directbytebuffer。开始gc会释放掉部分空间,但是后面越来越频繁,直到一直fgc。说明分配的堆外空间已经被全部使用,每次申请directbytebuffer都会导致fgc。 

通过日志发现使用的是netty的buffer pool,基本上可以确定是某个地方在拿到directbytebuffer,没有归还导致的堆外内存泄露。

2 fgc日志分析

[full gc2016-11-26t00:07:22.259+0800: 128.262: [cms: 43613k->43670k(4194304k), 0.1500870 secs] 82477k->43670k(7969216k), [cms perm : 29810k->29810k(262144k)], 0.1501840 secs] [times: user=0.15 sys=0.00, real=0.15 secs]

上述日志时候调用system.gc打印出来的。可以看到user=real=0.15,可知是只有一个线程在进行fgc,导致stw。

有如下问题 

 1 :  user和real有什么区别

 2:是哪一个线程在进行fgc

 3:这里fgc是否有优化的空间

real代表真实的时间消耗,user代表是所有cpu相加的时间消耗。如果相等,说明只有一个线程在gc。 如果日志如下:[times: user=0.31 sys=0.00, real=0.03 secs] 说明将近有10个线程在进行gc。

system.gc(),实际上是封装了一个_java_lang_system_gc操作放到vmthread的队列上,vmthread会扫描队列,做对应的操作。这里可以判定fgc的线程是vmthread。

vmthread线程是jvm线程,该线程会一直扫描vm_operation的队列。内存分配失败或者system.gc等,都会封装一个操作放到vm_operation队列上。vmthread在对gc等操作执行的时候,会让业务线程都进入到安全点进行阻塞。操作完成后,会让业务线程离开安全点继续做业务操作。

链接:http://calvin1978.blogcn.com/articles/safepoint.html

在old区是cms的情况下影响system.gc()的主要有2个jvm属性:disableexplicitgc和explicitgcinvokesconcurrent

链接:http://lovestblog.cn/blog/2016/08/29/oom/

下面对gc做一个梳理(jdk7和开启cms)

千丝万缕的FGC与Buffer pool

两个箭头代表多个线程,一个箭头代表单线程。

fgc(并行)前提是开启了explicitgcinvokesconcurrent参数。

3 触发fgc条件

1:正确情况下对象创建需要分配的内存是来自于heap的eden区域里,当eden内存不够用的时候,某些情况下会尝试到old里进行分配(比如说要分配的内存很大),如果还是没有分配成功,于是会触发一次ygc的动作,而ygc完成之后会再次尝试分配,如果仍不足以分配此时的内存,那会接着做一次fgc (不过此时的soft reference不会被强制回收),将老生代也回收一下,接着再做一次分配,仍然不够分配那会做一次强制将soft reference也回收的fgc,如果还是不能分配,则抛出outofmemoryerror。

2:system.gc

3:jmap -histo:live

4:jvisualvm 操作gc

5:担保失败

6:perm空间满

通过前面分析,一直fgc的原因是directbytebuffer泄露,导致一直调用system.gc,下面将轮到本文的主角登场,堆外buffer。

4 堆外buffer

堆外buffer一般是指:directbytebuffer

4.1 directbytebuffer优点

1:支持更大的内存

2:减轻对gc的影响(避免拷贝)

    比如ygc,会将eden区有引用的对象拷贝到s0或者s1,堆内buffer的数据会频繁拷贝。

3:减轻fgc的压力

     fgc在old区一般会进行数据整理。 在整理的时候,会进行内存数据的迁移。由于buffer的空间比较大,会导致迁移的时间较长。

4:降低fgc的频率

     buffer放在堆内,则old区占用的空间比较大,容易触发fgc。

5:网络通信降低数据拷贝

   通过nio 的channel write一个buffer:socketchannel.write(bytebuffer src) ,会调用如下接口:

千丝万缕的FGC与Buffer pool

如果是heapbytebuffer,还是要转换为的directbytebuffer,多一次数据拷贝。

4.2 directbytebuffer介绍

4.2.1 directbytebuffer结构

千丝万缕的FGC与Buffer pool

4.2.2 directbytebuffer结构

新建一个对象,一般情况下jvm在gc的时候会将对象回收掉。如果关联其他资源,在回收后,还需要将其释放。 比如回收掉directbytebuffer,还要释放掉申请的堆外空间。

1 finalize回收方案

sun不推荐实现finalize,实际上jdk内部很多类都实现了finalize。

千丝万缕的FGC与Buffer pool

如果对象实现了finalize,在对象初始化后,会封装成finalizer对象添加到 finalizer链表中。

对象被gc时,如果是finalizer对象,会将对象赋值到pending对象。reference handler线程会将pending对象push到queue中。

finalizer线程poll到对象,先删除掉finalizer链表中对应的对象,然后再执行对象的finalize方法(一般为资源的销毁)

方案的缺点:

  1:对象至少跨越2个gc,垃圾对象无法及时被gc掉,并且存在多次拷贝。影响ygc和fgc

  2:finalizer线程优先级较低,会导致finalize方法延迟执行

链接:http://www.infoq.com/cn/articles/jvm-source-code-analysis-finalreference

2 cleaner方案

千丝万缕的FGC与Buffer pool

比如创建directbytebuffer,会新建cleaner对象,该对象添加到cleaner链表中。

对象被gc,如果是cleaner对象,则会执行该对象的clean方法

clean方法会将对应的cleaner对象从链表中移除,同时会回收directbytebuffer申请的资源。

3 对比

两种方案都是对象被gc后,获取通知,然后对关联的资源进行销毁。

其实就是对象被gc后的notification的实现。cleaner采用的类似于push的方案,finalize采用类似于pull的方案。

cleaner方案相对finalize的方案性能较高,directbytebuffer选择了cleaner方案来实现资源的销毁

链接:http://calvin1978.blogcn.com/articles/directbytebuffer.html

4.3 directby特buffer缺点

1:api接口复杂

   区分写模式和读模式,指针移位操作比较复杂。

2:申请或者释放有同步

   在申请资源和释放资源时存在synchronized同步控制,主要是对已分配空间大小进行同步控制。

3:堆外空间得不到及时释放

   只有gc才会对不存在reference的directbytebuffer对象进行回收。 如果directbytebuffer对象已经到old区了并且已经不存在reference,那么ygc是不能对buffer申请的资源做回收,只有fgc才能进行回收。如果堆外空间不够用了,但是old区directbytebuffer对象持有的堆外空间得不到释放,容易导致fgc。

4:停顿时间较长

  堆外空间不够会进行休眠: sleep 100ms 。

千丝万缕的FGC与Buffer pool

很多文章都有提到会休眠100ms,特别对响应时间敏感的系统,影响比较大。这里需要思考为什么要设计休眠100ms,如果不休眠又会有什么问题?

如下图:(开启了cms )

千丝万缕的FGC与Buffer pool

  1:system.gc的仅仅是封装成一个vmoperation,添加到vmoperationqueue。vmthread会循环扫描queue, 执行vmoperation。

  2:这里也可以看到是否设置explicitgcinvokesconcurren 参数,会封装成不同的vmoperation。

 如果不sleep,由于system.gc仅仅是添加队列,很容易导致还没有fgc,就执行了后面的throw new outofmemoryerror("direct buffer memory");   

 这里设置100ms,应该是能够保证封装的任务能够被vmthread线程执行。当然,如果在100ms内,vmthread还未执行到fgc的vmoperation,也会直接抛出outofmemoryerror。

4.4 jna

directbytebuffer是使用unsafe(jni)申请堆外空间(unsafe.allocatememory(size))。还有一种申请堆外空间的手段:jna。

堆外缓存ohc便是使用jna来申请堆外空间。

4.5 netty的bytebuf

千丝万缕的FGC与Buffer pool

可以看到netty的directbytebuf底层的实现是jdk的directbytebuffer,仅仅是对jdk的directbytebuffer做了api的封装。

netty directbytebuf的特性:

1:易用的api接口。

2:channel发送buffer的时候,还需要将bytebuf转换为jdk的bytebuffer进行发送。

5 buffer pool

5.1 buffer pool设计准则

设计准则:易用性,性能,碎片化,同步,高利用率,安全性

易用性:提供的接口是否方便。比如malloc,free使用的便利性,buffer接口的易用性。

性能:buffer获取及归还是否高效。

碎片化:减少内存碎片化,提升小内存的使用效率

同步:在多线程环境下,如何保证内存分配的正确性,避免同一块内存同时对多个线程使用。主要同步点有:bufferpool同步,directbytebuffer申请同步,系统的malloc申请同步

高利用率:提升内存使用率,内存尽可能服务更多的请求。比如申请1k大小,如果返回了8k大小的buffer,就明显存在内存浪费的情况。

安全性:避免内存泄露。比如线程销毁候,线程上的内存是否及时归还。比如业务malloc一块内存,如何检测是否进行了free。

5.2 mycat buffer pool

下面以mycat 1.4.0版本的buffer pool进行描述:

千丝万缕的FGC与Buffer pool

规则:

  1:buffer默认是directbytebuffer

  2:buffer大小默认是4k

  3:buffer pool启动会进行初始化。默认申请buffer数量为:4k *1000*processer

申请buffer流程:

  1:申请buffer如果大于4k,则会从堆内进行申请,不进行池化。

  2:小于等于4k,则先从threadlocal申请,如果存在,直接返回。

  3:如果threadlocal不存在,则从bufferpool申请,如果存在,则直接返回。由于该处是全局访问,需要做同步控制。mycat使用concurrentlinkedqueue 来管理buffer。

  4:bufferpool没有可用的buffer,则会到jvm中进行bytebuffer.allocatedirect(chunksize)

其他细节这里不再讨论,比如buffer的释放流程等。

缺点:

  1:内存浪费:如果需要的大小是很小,比如100。但是每次都需要申请4k的空间

  2:性能较低:比如申请大于4k的空间,都要在堆内进行申请,性能较低

  3:安全性:没有内存泄露的检查机制。比如未归还等。

mycat在1.6.0版本,对bufferpool的管理做了优化,此处暂不做讲解。

5.3 netty的buffer pool

netty4实现了一套java版的jemalloc(buddy allocation和slab allocation),来管理netty的buffer。

下面先看一下twitter对netty buffer pool的性能测试。

千丝万缕的FGC与Buffer pool

 可以看到随着申请size的增加,pooled的性能优势越来越明显。

5.4 buffer pool使用

申请buffer:allocator.directbuffer(int size) ,使用完成后,必须要release。

如何确定申请buffer的size?

已知大小申请

    如果很清楚需要申请buffer的size,则直接申请对应的size。一般使用场景是io数据的发送。先将对象序列化为byte[],再从bufferpool里面申请对应大小的buffer,将byte[]拷贝到buffer中,发送buffer。

未知大小申请

   从channel中read数据,但是read之前并不确定buffer的大小,有两种方式申请buffer的size

   fixedrecvbytebufallocator:固定大小。比如固定申请4k的buffer。

   adaptiverecvbytebufallocator:自适应大小。可以通过历史申请的buffer size情况预测下一次申请的buffer size。

5.5 buffer pool设计

5.5.1 层级概念

千丝万缕的FGC与Buffer pool

poolarena :作为内存申请和释放的入口。负责同步控制

poolchunklist :负责管理chunk的生命周期,提升内存分配的效率

poolchunk :主要负责内存块的分配和回收。默认内存块大小为8k*2k=16m。该大小为从内存中申请的连续空间。

page :可以分配的最小内存单元 ,默认是8k。

poolsubpage :将page进行拆分,减少内存碎片,提升内存使用效率。

5.5.2 buffer申请流程

千丝万缕的FGC与Buffer pool

申请buffer流程

1:从threadlocal中拿到对应的poolarena,由于多个线程可能公用一个poolarena,需要考虑同步。

2:通过传入的size,计算申请的buffer大小(normalizecap)。并不是申请多大的size,就返回多大的size。

3:如果size>16m,则申请unpool的buffer。否则从threadlocal里面获取对应的buffer。

4:threadlocal里没有空闲的buffer,则会从全局的bufferpool中申请buffer。

容量管理

内存分为tiny(16,512)、small[512,8k)、normal[8k,16m]、huge[16m,)这四种类型。其中tiny和small在一个page里面分配。normal是多个page组成。huge是单独分配。

tiny:属于poolsubpage,大小是从16开始,每次增加16字节。中间总共有32个不同值

small:属于poolsubpage,大小是从512开始,每次增加2倍,总共有4个不同值

normal:属于page,大小是从8k开始,一直到16m,每次增加2倍,总共有11个不同值

huge: 如果判断用户申请的空间大于16m,则不会使用pool,直接申请unpool的buffer。(大内存不方便pool管理)

threadlocal容量管理

千丝万缕的FGC与Buffer pool

1:为了提升性能,在threadlocal里对tiny,small,normal进行缓存。

2:比如tiny,数组中由32个不同的大小。其中每个大小会缓存512份相同大小的buffer。如果超过了512,则不进行缓存 。small和normal类似。

poolarena容量管理

千丝万缕的FGC与Buffer pool

申请的size是tiny或者small :poolarena会缓存tiny和small的buffer。从arena里面的tinypool和smallpool申请buffer。

tiny或者small数组每一位代表不同大小的buffer,比如tiny的第0个数组代表size为16的buffer。其中数组是指向subpage的链表。链表的每一个都是总大小为8k,按照size等分的subpage(比如tiny的第0个数组,指向的subpage,全部都是总大小为8k,里面每一个element为16)

申请的size是normal[8k,16m]:则会从poolchunklist分配,如果分配不到,则会申请一块连续的8k*2k=16m directbytebuffer作为一个chunk。再从新申请的chunk中分配normal的buffer。同时将chunk添加到poolchunklist。

5.5.3 chunklist管理

链接:http://blog.csdn.net/youaremoon/article/details/48085591

5.5.4 chunk管理

千丝万缕的FGC与Buffer pool

5.5.5 内存泄漏方案

netty的bufferpool为了提高内存分配的效率,使用了threadlocal来缓存buffer。这里存在一个问题,如果线程注销掉了,如果不把缓存的buffer进行释放,则会存在内存泄露。

netty会将缓存buffer的线程进行watch,如果发现watch的线程会注销掉,便会释放掉缓存在threadlocal里面的buffer,来避免内存泄露

分配跟踪

非常好奇,如果业务线程申请了buffer,未做归还,如何监控发现。我尝试去设计该监控方案,但是一直未有好的思路,发现netty此处设计的非常巧妙。

千丝万缕的FGC与Buffer pool

 simple采样场景

申请buffer,其实返回的是simpleleakawarebytebuf对象。该对象里面包含了从bufferpool中申请的bytebuf和defaultresourceleak的幻引用对象。

如果业务线程已经不存在对simpleleakawarebytebuf对象的引用(业务线程对申请的buf使用完成),在gc的时候,便会回收 simpleleakawarebytebuf对象,同时也会回收defaultresourceleak对象,由于bytebuf还存在bufferpool的strong ref,不会进行回收。

由于defaultresourceleak是幻引用,gc时,便会将defaultresourceleak对象添加到refqueue 队列中。

如果业务处理正常release了,则会对defaultresourceleak标识为close状态。

每次申请buffer时,先查看refqueue里面是否有被回收的defaultresourceleak对象。如果有,判断是否是close状态(代表进行了release),否则存在了内存泄露(业务线程没有调用release方法)。

主要是通过引用关系来巧妙的实现来监控buffer申请未归还。

5.5.6 buffer pool的缺点

netty buffer pool带来了性能等各方面优势的同时,它的缺点也非常突出。

1:在申请buffer的同时,还要记得release。

    本文前面提到线上的fgc,便是由于申请的buffer没有release,导致内存泄露。这里其实打破由jvm负责没有引用对象的回收机制。就像江南白衣所说:一下又回到了c的冰冷时代。

2:业务逻辑结构复杂

    申请的buffer可能会存在跨很多方法的传递,也可能会对申请buffer进行slice, copy 等操作,还需要注意异常的处理。导致释放操作是一件非常复杂的事情。

3:bufferpool实现复杂

    从前面的buffer pool实现可以看出,为了达到内存的有效分配,内存分配的性能,实现非常复杂。虽然框架屏蔽了复杂性,但是对于实现原理的了解还是很困难。

分享者简介:何涛,唯品会架构师。就职于唯品会平台架构部,负责数据访问层,网关,数据库中间件,平台框架等开发设计工作。在数据库性能优化,架构设计等方面有大量经验,热衷于高可用,高并发及高性能的架构研究。