1 背景
運維通知,線上系統一直在fgc,通過zabbix檢視gc 的次數
再檢視ygc和fgc空間占用情況
這裡有幾個疑問:
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是對整個堆進行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(并行)前提是開啟了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) ,會調用如下接口:
如果是heapbytebuffer,還是要轉換為的directbytebuffer,多一次資料拷貝。
4.2 directbytebuffer介紹
4.2.1 directbytebuffer結構
4.2.2 directbytebuffer結構
建立一個對象,一般情況下jvm在gc的時候會将對象回收掉。如果關聯其他資源,在回收後,還需要将其釋放。 比如回收掉directbytebuffer,還要釋放掉申請的堆外空間。
1 finalize回收方案
sun不推薦實作finalize,實際上jdk内部很多類都實作了finalize。
如果對象實作了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方案
比如建立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 。
很多文章都有提到會休眠100ms,特别對響應時間敏感的系統,影響比較大。這裡需要思考為什麼要設計休眠100ms,如果不休眠又會有什麼問題?
如下圖:(開啟了cms )
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
可以看到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進行描述:
規則:
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的性能測試。
可以看到随着申請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 層級概念
poolarena :作為記憶體申請和釋放的入口。負責同步控制
poolchunklist :負責管理chunk的生命周期,提升記憶體配置設定的效率
poolchunk :主要負責記憶體塊的配置設定和回收。預設記憶體塊大小為8k*2k=16m。該大小為從記憶體中申請的連續空間。
page :可以配置設定的最小記憶體單元 ,預設是8k。
poolsubpage :将page進行拆分,減少記憶體碎片,提升記憶體使用效率。
5.5.2 buffer申請流程
申請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容量管理
1:為了提升性能,在threadlocal裡對tiny,small,normal進行緩存。
2:比如tiny,數組中由32個不同的大小。其中每個大小會緩存512份相同大小的buffer。如果超過了512,則不進行緩存 。small和normal類似。
poolarena容量管理
申請的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管理
5.5.5 記憶體洩漏方案
netty的bufferpool為了提高記憶體配置設定的效率,使用了threadlocal來緩存buffer。這裡存在一個問題,如果線程登出掉了,如果不把緩存的buffer進行釋放,則會存在記憶體洩露。
netty會将緩存buffer的線程進行watch,如果發現watch的線程會登出掉,便會釋放掉緩存在threadlocal裡面的buffer,來避免記憶體洩露
配置設定跟蹤
非常好奇,如果業務線程申請了buffer,未做歸還,如何監控發現。我嘗試去設計該監控方案,但是一直未有好的思路,發現netty此處設計的非常巧妙。
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實作可以看出,為了達到記憶體的有效配置設定,記憶體配置設定的性能,實作非常複雜。雖然架構屏蔽了複雜性,但是對于實作原理的了解還是很困難。
分享者簡介:何濤,唯品會架構師。就職于唯品會平台架構部,負責資料通路層,網關,資料庫中間件,平台架構等開發設計工作。在資料庫性能優化,架構設計等方面有大量經驗,熱衷于高可用,高并發及高性能的架構研究。