導(dǎo)讀
在java的世界里好像已經(jīng)不再需要關(guān)注內(nèi)存申請(qǐng)、內(nèi)存回收這些直接內(nèi)存操作了仍翰。更多的時(shí)候java程序員是在討論垃圾回收器(內(nèi)存分代赫粥、回收算法)等內(nèi)存自動(dòng)回收技術(shù)。操作系統(tǒng)中各種相關(guān)名詞“內(nèi)存池”予借、“內(nèi)存碎片”越平、“malloc”、“free”似乎早已遠(yuǎn)去灵迫。
本文借助于內(nèi)存池的java實(shí)現(xiàn)—Netty內(nèi)存池秦叛,對(duì)相關(guān)理論和實(shí)現(xiàn)進(jìn)行了闡述以使我們重新復(fù)習(xí)一下內(nèi)存分配和回收的相關(guān)知識(shí)。
背景
計(jì)算機(jī)世界中存在著各種各樣的“池子”瀑粥,如線程池挣跋、連接池、內(nèi)存池狞换。在java世界中可能接觸的最多的是線程池/連接池避咆,內(nèi)存池好像比較陌生,更多的時(shí)候?qū)?nèi)存池的印象只停留在上學(xué)時(shí)期操作系統(tǒng)課程中的內(nèi)存管理相關(guān)內(nèi)容修噪。因此本文要講的主要內(nèi)容為內(nèi)存池的一個(gè)java實(shí)現(xiàn)—Netty內(nèi)存池查库。為了能較明白的理解Netty內(nèi)存池的設(shè)計(jì)和實(shí)現(xiàn),本文從以下幾個(gè)方面對(duì)內(nèi)存池進(jìn)行了分析黄琼。
1樊销,為什么需要內(nèi)存池;
2脏款,內(nèi)存池的實(shí)現(xiàn)思路和理論支持现柠;
3,Netty內(nèi)存池的總體介紹弛矛;
4够吩,Netty內(nèi)存池關(guān)鍵組件分析。
為什么需要內(nèi)存池
在做一件事情前如果不了解它存在的意義那么必將陷入“只見(jiàn)樹(shù)木不見(jiàn)森林”的誤區(qū)丈氓。因此弄清楚內(nèi)存池存在的意義會(huì)是很有必要的周循。這里先不直接去說(shuō)明為什么需要內(nèi)存池,可以舉例思考下面幾個(gè)場(chǎng)景万俗。
直觀上感覺(jué)java中內(nèi)存的分配和回收完全是由jvm所控制的湾笛,jvm對(duì)內(nèi)存管理高效并且方便(不需要要求程序員時(shí)刻想著內(nèi)存釋放)。的確闰歪,jvm內(nèi)存分配確實(shí)是高效的嚎研,但是是否所有的場(chǎng)景下jvm的自動(dòng)的分配和回收都是高效的呢?是否還有著更好的實(shí)現(xiàn)方案?對(duì)于有著更高要求的情況也是這樣的嗎?對(duì)上面幾個(gè)問(wèn)題分別簡(jiǎn)單思考一下(當(dāng)然這并不是要讀者聯(lián)想到如C++語(yǔ)言的手動(dòng)內(nèi)存分配和回收,僅僅是針對(duì)java中的一些典型情況)。
1)是否所有的場(chǎng)景下jvm的自動(dòng)的分配和回收都是高效的呢??
對(duì)于需要使用緩沖區(qū)Buffer的io操作來(lái)講临扮,情況卻稍有不同论矾。這主要體現(xiàn)在兩個(gè)方面:頻繁的內(nèi)存Buffer的創(chuàng)建銷(xiāo)毀這必然會(huì)對(duì)jvm的GC造成一定的壓力,進(jìn)而影響服務(wù)的性能杆勇,因?yàn)榧热皇莍o密集型的業(yè)務(wù)為什么不cache一份呢贪壳;如果對(duì)內(nèi)存使用有著更高的要求,如使用堆外直接內(nèi)存的分配和回收蚜退,雖然堆外內(nèi)存的使用效率會(huì)比較高闰靴,但是其內(nèi)存申請(qǐng)的操作會(huì)比堆內(nèi)存的申請(qǐng)更加耗時(shí),因此頻繁的申請(qǐng)和釋放將是一件更加耗時(shí)的操作钻注。?
2)是否還有著更好的實(shí)現(xiàn)方案?
受限于當(dāng)下的軟件和硬件的處理速度或許一時(shí)想不到更好的方案對(duì) 這些特殊的場(chǎng)景進(jìn)行優(yōu)化蚂且,這時(shí)我們可以借助于一些計(jì)算機(jī)領(lǐng)域的傳統(tǒng)理論“內(nèi)存池”。進(jìn)而就是java語(yǔ)言實(shí)現(xiàn)的內(nèi)存池-Netty內(nèi)存池幅恋。針對(duì)上面的情況可以看一下一個(gè)比較權(quán)威的官方測(cè)試結(jié)果-Twitter使 用Netty內(nèi)存池和不使用內(nèi)存池的性能對(duì)比(ps為什么算是比較權(quán)威官方的測(cè)試呢?因?yàn)镹etty的創(chuàng)始人加入了twitter杏死,并且Netty在其公司內(nèi)部被廣泛使用)。
?? ?圖1:內(nèi)存分配池化和非池化對(duì)比
(出自twitter技術(shù)博客netty-4-at-twitter-reduced-gc-overhead)
從上圖中可以清楚的看到隨著內(nèi)存需求的逐漸增大佳遣,無(wú)論是堆內(nèi)存還是堆外直接內(nèi)存耗費(fèi)時(shí)間的差距越來(lái)越大识埋。池化技術(shù)確實(shí)展現(xiàn)了不錯(cuò)的性能凡伊。
怎么實(shí)現(xiàn)一個(gè)內(nèi)存池
上一部分對(duì)內(nèi)存池存在的必要性進(jìn)行說(shuō)明零渐,此部分主要說(shuō)明下如何實(shí)現(xiàn)一個(gè)內(nèi)存池。先不去參考其他任何成熟的線程池方案系忙,可以先用我們樸素的想法去思考一下這個(gè)問(wèn)題诵盼。這里舉一個(gè)常規(guī)思路的例子并說(shuō)說(shuō)在此例子中可能會(huì)遇到的問(wèn)題。
1)預(yù)先去申請(qǐng)一塊內(nèi)存(比如java中去new一個(gè)byte數(shù)組)然后緩存起來(lái)一直不釋放;
2)外部有對(duì)內(nèi)存池的內(nèi)存申請(qǐng)請(qǐng)求時(shí)银还,這時(shí)候需要去byte數(shù)組中選取一塊可用的內(nèi)存返回給使用方风宁。
上面簡(jiǎn)單的兩步似乎就完美的解決了內(nèi)存池的問(wèn)題。但再深入思考一下就會(huì)發(fā)現(xiàn)在分配的過(guò)程中就會(huì)面臨幾個(gè)典型的問(wèn)題:
1)采用什么樣的數(shù)據(jù)結(jié)構(gòu)和算法組織管理內(nèi)存的使用和未使用情況?
2)在多線程并發(fā)申請(qǐng)的情況下如何盡量保證和單線程一樣的效率?
3)隨著不斷的分配內(nèi)存蛹疯,回收內(nèi)存這些操作會(huì)不會(huì)造成內(nèi)存空間的不連續(xù)性也就是內(nèi)存碎片戒财,如何避免這些問(wèn)題?
4)進(jìn)一步還有更高級(jí)一點(diǎn)的問(wèn)題,有沒(méi)有機(jī)制來(lái)保證不發(fā)生內(nèi)存泄露的情況(這里指本應(yīng)該釋放回內(nèi)存池的內(nèi)存因?yàn)槟撤N錯(cuò)誤一直 占據(jù)著沒(méi)有釋放)捺弦。?
通過(guò)上面幾個(gè)問(wèn)題饮寞,進(jìn)而可以得出評(píng)價(jià)內(nèi)存池好壞指標(biāo)的兩個(gè)重要的因素:?
1)快速的申請(qǐng)和釋放內(nèi)存;?
2)盡可能的提高內(nèi)存的利用率,減少內(nèi)存碎片列吼。
自己思考半天可能未必會(huì)是一個(gè)較優(yōu)的方案幽崩,這時(shí)候需要來(lái)看看一個(gè)優(yōu)秀的內(nèi)存池實(shí)現(xiàn)-jemalloc,這個(gè)也是Netty內(nèi)存池實(shí)現(xiàn)的理論依據(jù)和參考寞钥。了解完jemalloc后再看Netty源代碼中那些晦澀的術(shù)語(yǔ)和莫名其妙的算法才有事半功倍的作用慌申,否則只會(huì)是事倍功半。?
簡(jiǎn)單的介紹一下jemalloc理郑,jemalloc由Jason Evans(當(dāng)時(shí)FreeBSD的一個(gè)厲害的開(kāi)發(fā)人員)在2005年為提高低性能的malloc而開(kāi)發(fā)的jemalloc蹄溉,此后Jason Evans加入Facebook并在2009年針對(duì) Facebook的使用情況做了一系列的適配和優(yōu)化后應(yīng)用到了Facebook內(nèi)部的很多項(xiàng)目中咨油,同時(shí)jemalloc也因?yàn)镕acebook變的很出名。
(http://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc. pdf)為Jason Evans在2006年發(fā)表的論文原文类缤。
jemalloc同樣也是站在了malloc的肩膀之上臼勉,但是jemalloc發(fā)展出最突出的兩個(gè)特點(diǎn):多Arena和threadlocal-cache。多Arena是將整個(gè)內(nèi)存池的總體內(nèi)存容量劃分成幾個(gè)小塊來(lái)管理餐弱,這個(gè)塊就是Arena宴霸。多個(gè)Arena可以分散多線程的請(qǐng)求,減少Arena上的內(nèi)存競(jìng)爭(zhēng)膏蚓。threadlocal-cache為線程級(jí)別緩存瓢谢,每個(gè)線程有自己的內(nèi)存緩存,這樣可以完全避免多線程的鎖競(jìng)爭(zhēng)驮瞧,同時(shí)也減少了內(nèi)存池內(nèi)存分配的總體流程氓扛,減少了內(nèi)存分配的時(shí)間復(fù)雜度。
Netty中的內(nèi)存池
在網(wǎng)絡(luò)IO中會(huì)涉及到大量的讀寫(xiě)操作论笔,從而也會(huì)涉及到大量的內(nèi)存申請(qǐng)和回收操作采郎。Netty雖然由java語(yǔ)言開(kāi)發(fā)但其“重回原點(diǎn)”開(kāi)發(fā)了一個(gè)內(nèi)存池用于內(nèi)存的分配和回收來(lái)進(jìn)一步提高性能。本文內(nèi)容基于Netty4.1.43.Final版本(5.x版本官方承認(rèn)效果不佳不再支持狂魔,如fork-join線程池的優(yōu)化失敗)蒜埋。??
1.Netty?內(nèi)存池總體概覽
在了解一件事的時(shí)候往往會(huì)先了解一下helloworld,這次也不例外最楷,下面為Netty內(nèi)存池的使用例子整份。
publicstaticvoidmain(String[] args){// 默認(rèn)為堆內(nèi)存,Netty內(nèi)存池可以為堆內(nèi)存籽孙,也可以為堆外內(nèi)存ByteBufAllocator alloc =newPooledByteBufAllocator();// 申請(qǐng)3M的的堆外內(nèi)存ByteBuf byteBuf = alloc.directBuffer(3145728);// 內(nèi)存釋放? ? ? ? byteBuf.release();// 申請(qǐng)12字節(jié)的堆內(nèi)存ByteBuf heapBuf = alloc.buffer(12);? ? ? ? heapBuf.release();? ? }
為了避免讓讀者陷入N多不知所云的術(shù)語(yǔ)和無(wú)盡的源代碼細(xì)節(jié)中烈评,因此先根據(jù)jemalloc來(lái)總體上介紹一下Netty內(nèi)存池。首先需要介紹Netty內(nèi)存池中的幾個(gè)關(guān)鍵組件犯建,之所以會(huì)有這些組件就是是因?yàn)榉抡諈⒖剂薺emalloc讲冠,而jemalloc又在實(shí)踐中取得了不錯(cuò)的表現(xiàn)。
?圖2:Netty?內(nèi)存池總體結(jié)構(gòu)
?PoolArena:內(nèi)存池分配的總體入口适瓦,內(nèi)存池的分配動(dòng)作都在這個(gè)類(lèi)中對(duì)外提供竿开。根據(jù)jemalloc設(shè)計(jì)思路Netty也采用多個(gè)PoolArena以應(yīng)對(duì)多線程的情況(Arena的數(shù)量為了和Netty中EventLoop的數(shù)量保持一致,都是采用cup核心數(shù)量?* 2)犹菇。這樣多個(gè)線程對(duì)內(nèi)存池有分配請(qǐng)求時(shí)可以使多個(gè)線程均勻的分配在多個(gè)PoolArena以減少競(jìng)爭(zhēng)德迹。
PoolThreadLocalCache:為了提高在多線程環(huán)境下的性能,Netty設(shè)計(jì)了一個(gè)線程變量揭芍,該線程變量緩存一定的分配空間胳搞,使得分配回收不用走全部的分配流程,避免了線程競(jìng)爭(zhēng)。有點(diǎn)類(lèi)似于java中ThreadLocal的概念肌毅,是不是也和JVM中的TLAB有一種似曾相識(shí)的感覺(jué)筷转。
PoolChunkList:將具有相同內(nèi)存使用率區(qū)間的Chunk集中在一起形成的一個(gè)Chunk列表。目的在于高效的分配和管理悬而。
PoolChunk:一個(gè)連續(xù)內(nèi)存空間的管理者呜舒。具體有堆內(nèi)Chunk和堆外Chunk,其默認(rèn)大小為16MB笨奠。
Page:Chunk中的最小內(nèi)存單位椰憋。Chunk將自身申請(qǐng)的連續(xù)內(nèi)存空間分割成相等大小的許多Page日矫。通過(guò)對(duì)Page的分配來(lái)完成內(nèi)存分配功能奥此。其默認(rèn)大小為8k账锹。
PoolSubpage:分為T(mén)iny和Small類(lèi)型的內(nèi)存,用來(lái)表示不同大小的內(nèi)存塊蔚袍。Tiny內(nèi)存大小范圍是16Byte到512Byte乡范,以16Byte為一個(gè)臺(tái)階,因此可以用于分配Tiny內(nèi)存的有512/16=32種啤咽。Small內(nèi)存大小分別為512Byte晋辆、1k、2k宇整、4k四種類(lèi)型的內(nèi)存塊大小瓶佳。
上面還是不可避免的介紹了幾個(gè)組件概念,如果讀者第一次讀到的話肯定也會(huì)比較懵没陡。在詳細(xì)介紹上面幾個(gè)組件前涩哟,先來(lái)看一下內(nèi)存池的總體分配流程:
1)內(nèi)存分配器(ByteBufAllocator)確定是堆外直接內(nèi)存還是堆內(nèi)存索赏,根據(jù)請(qǐng)求的內(nèi)存塊大小轉(zhuǎn)換為線程池支持的內(nèi)存塊大小(如500字節(jié)轉(zhuǎn)化為512字節(jié)盼玄、3M轉(zhuǎn)化為4M);?
2)先去線程級(jí)緩存(PoolThreadLocalCache)中查看自己對(duì)應(yīng)線程的緩存中是否有已經(jīng)分配好的內(nèi)存塊,如有的話就直接返回潜腻。PoolThreadLocalCache思想基本和ThreadLocal一樣埃儿,可以避免多線程的鎖競(jìng)爭(zhēng);
3)根據(jù)申請(qǐng)的內(nèi)存塊大小選擇不同的分配策略申請(qǐng)內(nèi)存,如
<512byte的請(qǐng)求會(huì)根據(jù)實(shí)際請(qǐng)求的內(nèi)存大小去tinySubpagePools數(shù)組中選擇合適的Tiny鏈表進(jìn)行分配;
<8KB的請(qǐng)求會(huì)根據(jù)實(shí)際請(qǐng)求的內(nèi)存大小去smallSubpagePools數(shù)組總選擇合適的Small鏈表進(jìn)行分配;
<=16MB的請(qǐng)求會(huì)去 PoolChunkList組團(tuán)中根據(jù)規(guī)則選取PoolChunkList中的PoolChunk進(jìn)行內(nèi)存分配融涣。
看完上面的關(guān)鍵組件介紹和總體內(nèi)存分配流程童番,或許你已經(jīng)對(duì)Netty內(nèi)存池有了一個(gè)總體上的印象。各個(gè)組件的一些細(xì)節(jié)會(huì)在后面的章節(jié)中進(jìn)行詳細(xì)的闡述威鹿。但是圖2中提到的“PoolChunkList 組團(tuán)”這里必須解釋一下剃斧,先來(lái)看jemalloc 論文中的一個(gè)截圖。
? ? ?圖?3:jemalloc論文中PoolChunkList組團(tuán)示意圖
? ?(出自論文A Scalable Concurrent malloc(3) Implementation for FreeBSD)
在PoolChunkList組團(tuán)中各個(gè)PoolChunkList如圖2中那樣通過(guò)雙向鏈表結(jié)合在一起忽你,各個(gè)PoolChunkList的PoolChunk會(huì)根據(jù)內(nèi)存使用率情況在各個(gè)PoolChunkList中移動(dòng)幼东,如圖3中按照chunk的使用率進(jìn)行了分類(lèi),并且根據(jù)論文中的建議內(nèi)存在PoolChunkList組團(tuán)中的分配順序Q50,Q25,Q0,Q75。Netty中基本也是按照這個(gè)順序只是在Q0和Q75之間增加了QINIT根蟹。之所以會(huì)存在“組團(tuán)”其目的是為了提高內(nèi)存的分配效率脓杉,并能夠及時(shí)回收掉不使用的內(nèi)存來(lái)控制總內(nèi)存池子的大小。如Q50保存的是內(nèi)存利用率50%至100%的Chunk简逮,首先在Q50中分配是一個(gè)綜合考慮分配效率和池子內(nèi)存利用率的折中方案球散。再比如Q75因?yàn)閮?nèi)存利用率太高,首先分配的必然會(huì)導(dǎo)致分配成功率降低從而整體上降低內(nèi)存分配效率散庶。
2.PoolChunk和伙伴算法
為了讓讀者有一個(gè)直觀的感受蕉堰,首先看一下PoolChunk中管理內(nèi)存池的數(shù)據(jù)結(jié)構(gòu)和算法。
?圖4:PoolChunk管理page數(shù)據(jù)結(jié)構(gòu)
? ? ? ? ? ? ? ? ?圖5:層數(shù)索引結(jié)構(gòu)圖?
如圖4悲龟,PoolChunk內(nèi)部管理著一個(gè)大塊的連續(xù)內(nèi)存區(qū)域嘁灯,將這個(gè)內(nèi)存區(qū)域切分成同樣大小的塊,每一個(gè)稱之為一個(gè)Page躲舌。從內(nèi)存池中申請(qǐng)大于等于8K內(nèi)存塊將會(huì)轉(zhuǎn)變?yōu)閺腜oolChunk中申請(qǐng)一定數(shù)量Page丑婿。為了方便內(nèi)存空間的管理,PoolChunk內(nèi)部采用完全二叉樹(shù)的方式對(duì)Page進(jìn)行管理没卸。通過(guò)PoolChunk羹奉,可以對(duì)大于等于PageSize(Page的大小默認(rèn)8k)的內(nèi)存申請(qǐng)進(jìn)行分配和管理。圖5中表示的是一個(gè)輔助滿二叉樹(shù)查詢層數(shù)的索引信息约计,在Netty中對(duì)應(yīng)于depthMap索引是為了便于快速根據(jù)page定位到層數(shù)诀拭。為了理解PoolChunk內(nèi)存分配算法需要記住PoolChunk的以下幾點(diǎn):?
1)一個(gè)PoolChunk默認(rèn)會(huì)申請(qǐng)16M大小的內(nèi)存空間;
2)page代表chunk中能被分配的最小單元煤蚌,page的大小為8k耕挨,因此?PoolChunk中共有2048個(gè)page;?
3)PoolChunk中的2048個(gè)page的開(kāi)辟數(shù)組(實(shí)際數(shù)組下標(biāo)從1開(kāi)始)按照滿二叉樹(shù)數(shù)組的存儲(chǔ)方式,把此數(shù)組看成一個(gè)邏輯上的二叉樹(shù)尉桩,此二叉樹(shù)共有最高層數(shù)11筒占。
在了解了PoolChunk內(nèi)部?jī)?nèi)存管理的數(shù)據(jù)結(jié)構(gòu)后,下面對(duì)PoolChunk分配流程/算法進(jìn)行介紹蜘犁。為了便于理解下面的流程以分配3M的內(nèi)存流程為例子進(jìn)行表述翰苫。?
1)由于3M大于2M小于4M,那么把3M的內(nèi)存請(qǐng)求轉(zhuǎn)換為4M進(jìn)行这橙;
2)此時(shí)為PoolChunk的初始分配奏窑,由圖可知二叉樹(shù)的第二層和4M空間相符合,這時(shí)取這層的最左節(jié)點(diǎn)page4節(jié)點(diǎn)作為內(nèi)存空間屈扎,并將高度值設(shè)置為12(樹(shù)的最大高度值+1)表示此節(jié)點(diǎn)以及此節(jié)點(diǎn)下的所有子節(jié)點(diǎn)都已經(jīng)分配完了埃唯;
3)page4節(jié)點(diǎn)的父親節(jié)點(diǎn)(page2節(jié)點(diǎn)),將高度值更新為兩個(gè)子節(jié)點(diǎn)的較小值鹰晨,直到高度值更新到根節(jié)點(diǎn)墨叛。父節(jié)點(diǎn)的高度值會(huì)相對(duì)原來(lái)的高度值變大滑沧,這代表該節(jié)點(diǎn)下的至少一個(gè)子節(jié)點(diǎn)或者孫子節(jié)點(diǎn)已經(jīng)被分配出去了。
上面的步驟基本描述了page內(nèi)存塊的分配過(guò)程巍实,但是有一點(diǎn)還需要補(bǔ)充說(shuō)明一下滓技,步驟3提到二叉樹(shù)節(jié)點(diǎn)被分配出去后更新了父節(jié)點(diǎn)并一直到根節(jié)點(diǎn),并沒(méi)有去更新子節(jié)點(diǎn)棚潦。如上個(gè)例子中page4節(jié)點(diǎn)已經(jīng)被分配令漂,但是page4下面的子節(jié)點(diǎn)/孫子節(jié)點(diǎn)的高度值并沒(méi)有更新,那么是如何保證這些子節(jié)點(diǎn)不會(huì)被再次分配到的呢? 這個(gè)Netty針對(duì)滿二叉樹(shù)做了一系列巧妙的位運(yùn)算得以解決這個(gè)問(wèn)題丸边。源代碼注釋如下:
上面寫(xiě)的內(nèi)容都是關(guān)于PoolChunk是如何分配內(nèi)存的叠必。但為什么要這樣設(shè)計(jì)呢?比如兩個(gè)主要的問(wèn)題:?
1)為什么要組織為二叉樹(shù)的形式?
2)為什么要組織為滿二叉樹(shù)?
在回答上面兩個(gè)問(wèn)題前先了解一下PoolChunk的理論依據(jù)-伙伴內(nèi)存分配算法。該算法的主要思路是每次把一個(gè)大內(nèi)存塊對(duì)半切分妹窖,然后再對(duì)半切分纬朝,一直切到最小的單位如PoolChunk的page(滿二叉樹(shù)可以方便的利用一塊大的連續(xù)內(nèi)存。分配的時(shí)候找到合適內(nèi)存塊大小即可;回收的時(shí)候骄呼,如果跟它的伙伴 (即PoolChunk二叉樹(shù)的兄弟節(jié)點(diǎn))也是未被使用的共苛,就合并成一個(gè)大的塊。數(shù)據(jù)結(jié)構(gòu)組織為二叉樹(shù)的形式蜓萄,那么分配和釋放的時(shí)間復(fù)雜度都是O(logN)同時(shí)N不會(huì)很大隅茎。該算法的優(yōu)點(diǎn)是可以有效減少內(nèi)存外部碎片;缺點(diǎn)是page為最小的內(nèi)存分配單位,那么如果分配請(qǐng)求不能被pageSize整除的情況下必然會(huì)產(chǎn)生內(nèi)存浪費(fèi)嫉沽。如上面的例子中申請(qǐng)的是3M的內(nèi)存空間辟犀,但是必須要給其分配一個(gè)4M的節(jié)點(diǎn),這就造成了有1M的內(nèi)存空間雖然被分配出去了绸硕,但是卻無(wú)法真正利用堂竟,這也就是內(nèi)部碎片。
知識(shí)小助手玻佩,內(nèi)部碎片和外部碎片出嘹。
外部碎片指的是還沒(méi)有被分配出去(不屬于任何進(jìn)程),但由于太小無(wú)法分配給申請(qǐng)內(nèi)存空間的進(jìn)程的內(nèi)存空閑區(qū)域夺蛇。這些存儲(chǔ)塊的總和可以滿足當(dāng)前申請(qǐng) 的長(zhǎng)度要求疚漆,但是由于它們的地址不連續(xù)或其他原因酣胀,使得無(wú)法分配出去刁赦。內(nèi)部碎片是指已經(jīng)被分配出去卻不能被利用的內(nèi)存空間,如上面Netty中PoolChunk的page例子闻镶。直到這個(gè)內(nèi)存空間被釋放后才有可能在下次分配中重新被真正利用甚脉。
3.PoolSubpage和Slab分配算法
通過(guò)上一個(gè)部分對(duì)PoolChunk的介紹得知,PoolChunk可以高效的對(duì)內(nèi)存進(jìn)行分配铆农,同時(shí)PoolChunk的存在有效的避免了外部碎片的產(chǎn)生牺氨。但是PoolChunk是否就完美了呢?顯然不是的狡耻。我們可以輕易的發(fā)現(xiàn)PoolChunk的兩個(gè)問(wèn)題。
1) PoolChunk中最小分配單位Page的大小為8K字節(jié)猴凹。但是如果小于PageSize的申請(qǐng)也要消耗掉一個(gè)Page的話就有些浪費(fèi)了夷狰。
2) PoolChunk雖然有效減少了外部碎片的產(chǎn)生,但是如果我們分配更小(小于8k)需求的情況下勢(shì)必有會(huì)造成內(nèi)存空洞或者內(nèi)存內(nèi)部碎片的產(chǎn)生郊霎。
針對(duì)上面的兩個(gè)問(wèn)題沼头,這時(shí)候有人或許會(huì)提出:把page的單元大小做的小一點(diǎn)不就行了嗎?比如page大小設(shè)置為4k书劝,2k进倍,1k甚至更小。那么這么做是否可行呢?只能說(shuō)這個(gè)解決方案在思路上有一定的道理(事實(shí)上幾種不同的伙伴系統(tǒng)的實(shí)現(xiàn)最小分配單位確實(shí)各不相同购对,不過(guò)應(yīng)該也沒(méi)有低于1k的)猾昆。
之所以最小單位不能設(shè)置的太小,說(shuō)說(shuō)我的理解:
1)page太小必然耗費(fèi)更多的內(nèi)存去管理大量的小塊內(nèi)存;?
2)雖然伙伴分配和釋放內(nèi)存時(shí)間復(fù)雜度是logN骡苞,但是隨著層數(shù)的增多以及不同大小內(nèi)存的混合分配必然造成二叉樹(shù)操作的冗余垂蜗;
3)經(jīng)過(guò)一段時(shí)間的分配和回收之后內(nèi)存碎片的問(wèn)題也更容易嚴(yán)重;
4)從統(tǒng)計(jì)實(shí)踐的角度去看這個(gè)問(wèn)題也不太合理解幽,即一個(gè)系統(tǒng)中不同大小內(nèi)存的應(yīng)用和使用方式肯定有著自己的規(guī)律么抗,因此實(shí)踐中會(huì)更適合“因材施教”。
Netty內(nèi)存池中PoolSubpage負(fù)責(zé)小內(nèi)存的管理分配亚铁,PoolSubpage的實(shí)現(xiàn)思路基本借鑒了SLAB內(nèi)存分配算法蝇刀,SLAB是一種針對(duì)小內(nèi)存塊申請(qǐng)回收的算法。SLAB往往也和伙伴分配機(jī)制配合使用徘溢,SLAB會(huì)在伙伴系統(tǒng)中申請(qǐng)一個(gè)最小內(nèi)存單元塊吞琐,然后把塊進(jìn)一步劃分為多個(gè)不同大小的內(nèi)存塊以滿足不同大小內(nèi)存分配的需求。針對(duì)小內(nèi)存塊的申請(qǐng)釋放可以只在SLAB中以一種更簡(jiǎn)單的方式運(yùn)行然爆。參考SLAB內(nèi)存分配器的特點(diǎn)站粟,Netty中PoolSubpage仿照SLAB思路設(shè)計(jì)了數(shù)據(jù)結(jié)構(gòu)。下面介紹PoolSubpage的結(jié)構(gòu)(總體上分為smallSubPagePool和tinysubPagePool)以及幾個(gè)重要特點(diǎn)曾雕。
? ? ? ? ? ? ? ? ? ??圖6: Small級(jí)別smallSubPagePool
?圖7: Tiny級(jí)別tinysubPagePool?
PoolSubpage其實(shí)是從PoolChunk中分配出來(lái)的一個(gè)page因此其大小也為8K奴烙。PoolSubpage也分為兩種類(lèi)型:小于512字節(jié)的塊類(lèi)型稱之為T(mén)iny,大于等于512字節(jié)的塊類(lèi)型稱之為Small剖张。
SmallSubPagePools(Small類(lèi)型的PoolSubpage鏈表)如圖6所示切诀,總共有四種不同的內(nèi)存大小塊,512字節(jié)搔弄、1024字節(jié)幅虑、2048字節(jié)、4096字節(jié)4個(gè)元素顾犹,每個(gè)元素對(duì)應(yīng)一個(gè)相同大小內(nèi)存塊的PoolSubpage鏈表倒庵。
TinySubPagePools(Tiny類(lèi)型的PoolSubpage鏈表)如圖7所示最小空間為16字節(jié)且16個(gè)字節(jié)為一個(gè)階梯褒墨,區(qū)間為[16,512)的一個(gè)數(shù)組,所以共有32種不同的內(nèi)存大小塊擎宝,因此也就會(huì)有32個(gè)元素郁妈,每個(gè)元素對(duì)應(yīng)一個(gè)相同大小內(nèi)存塊的PoolSubpage鏈表。
那么是如何有效管理PoolSubpage中的內(nèi)存塊的呢绍申?
簡(jiǎn)單的說(shuō)圃庭,Netty的實(shí)現(xiàn)方案是用bitMap管理PoolSubpage內(nèi)存塊是否已經(jīng)分配。如果bitMap對(duì)應(yīng)的位的值為1失晴,那么說(shuō)明已經(jīng)分配剧腻,如果為0說(shuō)明還沒(méi)有分配。這里需要特別說(shuō)明的是這里的bitMap為Netty利用long類(lèi)型數(shù)組實(shí)現(xiàn)方式涂屁。結(jié)合PoolSubpage內(nèi)存塊的分配流程和bitMap說(shuō)明如下:
如申請(qǐng)16字節(jié)的內(nèi)存塊:
1)判斷16字節(jié)的內(nèi)存塊為T(mén)iny類(lèi)型书在,那么去tinysubPagePool中中查找大小為16字節(jié)的PoolSubpage
2)如果為首次分配那么初始化PoolSubpage(內(nèi)存塊大小為16字節(jié))
3)初始化一個(gè)8個(gè)元素的long數(shù)組new long[8](總量為8k的PoolSubpage里面的小塊大小為16字節(jié),因此總共需要管理的小塊數(shù)量為512個(gè)拆又,512/64=8儒旬,64為long占用bit數(shù)量,因此需要8個(gè)元素)
4)在小塊內(nèi)存的分配過(guò)程中會(huì)按照bitMap中的順序依次分配帖族,如下
第1次分配取long數(shù)組(bitMap)的第1個(gè)元素栈源,并把long數(shù)字的最低位置為1;
第2次分配取long數(shù)組(bitMap)的第1個(gè)元素竖般,并把long數(shù)字的次低位置為1甚垦;
????.......
第65次分配取long數(shù)組(bitMap)的第2個(gè)元素,并把long數(shù)字的最低位置為1涣雕。
4.多線程和PoolThreadCache
在文章的前段提到Jemalloc內(nèi)存分配器設(shè)計(jì)的一個(gè)主要目標(biāo)是為了改善之前內(nèi)存分配器在多線程環(huán)境下不盡如人意的性能表現(xiàn)艰亮。因此Jemalloc除了利用多個(gè)Arena來(lái)減少線程競(jìng)爭(zhēng)的方式外,還有重要的一點(diǎn)是充分利用了線程級(jí)別緩存挣郭。針對(duì)線程級(jí)別緩存Netty設(shè)計(jì)了一個(gè)線程緩存類(lèi)PoolThreadCache通過(guò)線程變量的方式存儲(chǔ)迄埃。
當(dāng)程序申請(qǐng)空間也首先嘗試從緩存中申請(qǐng),內(nèi)存使用完畢后內(nèi)存空間也會(huì)添加到線程緩存PoolThreadCache中兑障。這里的線程級(jí)別緩存其實(shí)現(xiàn)思想非常類(lèi)似于java中的ThreadLocal并且Netty還設(shè)計(jì)了FastThreadLocal來(lái)進(jìn)一步提升性能侄非。PoolThreadCache的設(shè)計(jì)思路沿用Arena內(nèi)存空間分塊設(shè)計(jì)思想。但是在存儲(chǔ)空間和內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)上做了調(diào)整流译。
線程級(jí)緩存(PoolThreadLocalCache)和PoolThreadCache邏輯設(shè)計(jì)結(jié)構(gòu)如下圖:?
? 圖8:poolThreadCache結(jié)構(gòu)圖?
線程級(jí)緩存(PoolThreadLocalCache)中存儲(chǔ)了數(shù)據(jù)結(jié)構(gòu)PoolThreadCache逞怨。在PoolThreadCache創(chuàng)建有三種不同的MemoryRegionCache數(shù)組。分別為tiny數(shù)組先蒋、small數(shù)組骇钦、normal數(shù)組,這三者的含義和Arena定義的定義的三種數(shù)組的含義一致竞漾,代表著一個(gè)線程下緩存的不同大小的內(nèi)存塊眯搭。這里需要說(shuō)明的是為了控制線程棧內(nèi)占用太多的內(nèi)存,在PoolThreadCache中現(xiàn)在了存放內(nèi)存塊的大小业岁,如normal級(jí)別的內(nèi)存塊只允許cache最大32K的塊單位鳞仙。
當(dāng)要從線程緩存中申請(qǐng)空間時(shí),根據(jù)申請(qǐng)大小從MemoryRegionCache數(shù)組中找到合適的MemoryRegionCache笔时。如果能找到棍好,則從隊(duì)列中獲取節(jié)點(diǎn)得到內(nèi)存信息。當(dāng)要釋放內(nèi)存空間時(shí)允耿,優(yōu)先將內(nèi)存空間加入到緩存中借笙,也是先找到合適的MemoryRegionCache。如果能找到則嘗試將內(nèi)存信息放入隊(duì)列中较锡,如果隊(duì)列已滿业稼,則走正常的釋放流程。之所以采用隊(duì)列存儲(chǔ)內(nèi)存池信息一方面為了方便控制cache內(nèi)存的大新煸獭低散;另外也是為了方便快速的讀取緩存信息。
還有一點(diǎn)值得說(shuō)明是FastThreadLocal骡楼。FastThreadLocal是為了進(jìn)一步提高JDK當(dāng)中ThreadLocal的性能而開(kāi)發(fā)的工具類(lèi)熔号,其利用內(nèi)存對(duì)齊、優(yōu)化存取等一系列手段提升性能鸟整。當(dāng)然FastThreadLocal并不是本文的重點(diǎn)這里不做詳細(xì)展開(kāi)引镊。這里要想表達(dá)的是FastThreadLocal對(duì)提高Netty線程級(jí)緩存發(fā)揮了更極致的作用,因此它的設(shè)計(jì)和實(shí)現(xiàn)值得好好研究一下篮条。
總結(jié)
對(duì)于java程序員來(lái)說(shuō)內(nèi)存的手動(dòng)分配和回收并不常用祠乃,但隨著java中越來(lái)越多的高性能IO工具如緩存管理工具的廣泛應(yīng)用。熟悉系統(tǒng)中的內(nèi)存分配和回收原理會(huì)更加有利于掌握底層運(yùn)行的原理兑燥,加深系統(tǒng)的認(rèn)知亮瓷。
本文通過(guò)用內(nèi)存分配相關(guān)理論和Netty內(nèi)存池的實(shí)現(xiàn)聯(lián)系在一起的方式對(duì)內(nèi)存分配理論和實(shí)踐進(jìn)行介紹。雖然針對(duì)Netty內(nèi)存池源代碼方面的分析并不多降瞳,但是熟悉本文中的各種相關(guān)知識(shí)后再去閱讀源碼定會(huì)容易很多嘱支,同時(shí)也有助于我們?cè)O(shè)計(jì)出越來(lái)越優(yōu)秀的系統(tǒng)。
作者簡(jiǎn)介:
崔海東:用戶價(jià)值增長(zhǎng)部-部落后端技術(shù)
參考文獻(xiàn):
http://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf?
https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead?
http://jemalloc.net/?
Glibc內(nèi)存管理-Ptmalloc2源代碼分析
Netty源代碼
推薦閱讀:
開(kāi)源意見(jiàn)征集 |Taro React Native 提案
技術(shù)沙龍 | 第十七期 58同城風(fēng)控安全專(zhuān)場(chǎng)