Java中看內(nèi)存分配—Netty內(nèi)存池

導(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)

單聲道錄音中的角色識(shí)別優(yōu)化實(shí)踐

圖數(shù)據(jù)庫(kù)在58部落社交網(wǎng)絡(luò)的探索實(shí)踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挣饥,一起剝皮案震驚了整個(gè)濱河市除师,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扔枫,老刑警劉巖汛聚,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異短荐,居然都是意外死亡倚舀,警方通過(guò)查閱死者的電腦和手機(jī)叹哭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)痕貌,“玉大人风罩,你說(shuō)我怎么就攤上這事《娉恚” “怎么了超升?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)哺徊。 經(jīng)常有香客問(wèn)我室琢,道長(zhǎng),這世上最難降的妖魔是什么落追? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任盈滴,我火速辦了婚禮,結(jié)果婚禮上淋硝,老公的妹妹穿的比我還像新娘雹熬。我一直安慰自己,他們只是感情好谣膳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布竿报。 她就那樣靜靜地躺著,像睡著了一般继谚。 火紅的嫁衣襯著肌膚如雪烈菌。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天花履,我揣著相機(jī)與錄音芽世,去河邊找鬼。 笑死诡壁,一個(gè)胖子當(dāng)著我的面吹牛济瓢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妹卿,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼旺矾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了夺克?” 一聲冷哼從身側(cè)響起箕宙,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铺纽,沒(méi)想到半個(gè)月后柬帕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年陷寝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锅很。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盼铁,死狀恐怖粗蔚,靈堂內(nèi)的尸體忽然破棺而出尝偎,到底是詐尸還是另有隱情饶火,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布致扯,位于F島的核電站肤寝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏抖僵。R本人自食惡果不足惜鲤看,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望耍群。 院中可真熱鬧义桂,春花似錦、人聲如沸蹈垢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)曹抬。三九已至溉瓶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谤民,已是汗流浹背堰酿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留张足,地道東北人触创。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像为牍,于是被迫代替她去往敵國(guó)和親哼绑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355