天天看點

千絲萬縷的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實作可以看出,為了達到記憶體的有效配置設定,記憶體配置設定的性能,實作非常複雜。雖然架構屏蔽了複雜性,但是對于實作原理的了解還是很困難。

分享者簡介:何濤,唯品會架構師。就職于唯品會平台架構部,負責資料通路層,網關,資料庫中間件,平台架構等開發設計工作。在資料庫性能優化,架構設計等方面有大量經驗,熱衷于高可用,高并發及高性能的架構研究。