1 原理
應(yīng)用層的內(nèi)存分配最終是委托給PoolArena實現(xiàn)瞧预。先看下PoolArena的內(nèi)部數(shù)據(jù)結(jié)構(gòu):
poolArena提供了兩種方式進行內(nèi)存分配:
- PoolSubpage用于分配小于8k的內(nèi)存沿腰;
tinySubpagePools:用于分配小于512字節(jié)的內(nèi)存,默認長度為32伐坏,因為內(nèi)存分配最小為16逝慧,每次增加16截驮,直到512婆瓜,區(qū)間[16快集,512)一共有32個不同值;
smallSubpagePools:用于分配大于等于512字節(jié)的內(nèi)存廉白,默認長度為4个初;
tinySubpagePools和smallSubpagePools中的元素都是默認subpage。
- poolChunkList用于分配大于8k的內(nèi)存猴蹂;
qInit:存儲內(nèi)存利用率0-25%的chunk
q000:存儲內(nèi)存利用率1-50%的chunk
q025:存儲內(nèi)存利用率25-75%的chunk
q050:存儲內(nèi)存利用率50-100%的chunk
q075:存儲內(nèi)存利用率75-100%的chunk
q100:存儲內(nèi)存利用率100%的chunk
各chunkList連接如下:
按照內(nèi)存的使用率來取名的院溺,如qInit代表一個chunk最開始分配后會進入它,隨著其使用率增大會逐漸從q000到q100磅轻,而隨著內(nèi)存釋放覆获,使用率減小,它又會慢慢的從q100到q00,最終這個chunk上的所有內(nèi)存釋放后瓢省,整個chunk被回收。
接下來看下PoolArena如何進行內(nèi)存分配痊班,如下勤婚。
如果是分配小內(nèi)存,則嘗試從tinySubpagePools或smallSubpagePools中分配內(nèi)存涤伐,如果沒有合適subpage馒胆,則采用方法allocateNormal分配內(nèi)存缨称。
如果分配一個page以上的內(nèi)存,直接采用方法allocateNormal分配內(nèi)存祝迂。
默認都是先嘗試從poolThreadCache中分配內(nèi)存睦尽,PoolThreadCache利用ThreadLocal的特性,消除了多線程競爭型雳,提高內(nèi)存分配效率当凡;首次分配時,poolThreadCache中并沒有可用內(nèi)存進行分配纠俭,當上一次分配的內(nèi)存使用完并釋放時沿量,會將其加入到poolThreadCache中,提供該線程下次申請時使用冤荆。
內(nèi)存池內(nèi)存分配流程:
1朴则、ByteBufAllocator 準備申請一塊內(nèi)存;
2钓简、嘗試從PoolThreadCache中獲取可用內(nèi)存乌妒,如果成功則完成此次分配,否則繼續(xù)往下走外邓,注意后面的內(nèi)存分配都會加鎖撤蚊;
3、如果是小塊(可配置該值)內(nèi)存分配坐榆,則嘗試從PoolArena中緩存的PoolSubpage中獲取內(nèi)存拴魄,如果成功則完成此次分配;
4席镀、如果是普通大小的內(nèi)存分配匹中,則從PoolChunkList中查找可用PoolChunk并進行內(nèi)存分配,如果沒有可用的PoolChunk則創(chuàng)建一個并加入到PoolChunkList中豪诲,完成此次內(nèi)存分配顶捷;
5、如果是大塊(大于一個chunk的大惺豪椤)內(nèi)存分配服赎,則直接分配內(nèi)存而不用內(nèi)存池的方式;
6交播、內(nèi)存使用完成后進行釋放重虑,釋放的時候首先判斷是否和分配的時候是同一個線程,如果是則嘗試將其放入PoolThreadCache秦士,這塊內(nèi)存將會在下一次同一個線程申請內(nèi)存時使用缺厉,即前面的步驟2;
7、如果不是同一個線程提针,則回收至chunk中命爬,此時chunk中的內(nèi)存使用率會發(fā)生變化,可能導(dǎo)致該chunk在不同的PoolChunkList中移動辐脖,或者整個chunk回收(chunk在q000上饲宛,且其分配的所有內(nèi)存被釋放);同時如果釋放的是小塊內(nèi)存(與步驟3中描述的內(nèi)存相同)嗜价,會嘗試將小塊內(nèi)存前置到PoolArena中艇抠,這里操作成功了,步驟3的操作中才可能成功炭剪。
allocateNormal實現(xiàn)如下:
第一次進行內(nèi)存分配時练链,chunkList沒有chunk可以分配內(nèi)存,需通過方法newChunk新建一個chunk進行內(nèi)存分配奴拦,并添加到qInit列表中媒鼓。如果分配如512字節(jié)的小內(nèi)存,除了創(chuàng)建chunk错妖,還有創(chuàng)建subpage绿鸣,PoolSubpage在初始化之后,會添加到smallSubpagePools中暂氯,其實并不是直接插入到數(shù)組潮模,而是添加到head的next節(jié)點。下次再有分配512字節(jié)的需求時痴施,直接從smallSubpagePools獲取對應(yīng)的subpage進行分配擎厢。
這里為什么不是從較低的q000開始呢,我們知道一個chunk隨著內(nèi)存的不停釋放辣吃,它本身會不停的往其所在的chunk list的prev list移動动遭,直到其完全釋放后被回收。 如果這里是從q000開始嘗試分配神得,雖然分配的速度可能更快了(因為分配成功的幾率更大)厘惦,但一個chunk在使用率為25%以內(nèi)時有更大幾率再分配,也就是一個chunk被回收的幾率大大降低了哩簿。這樣就帶來了一個問題宵蕉,我們的應(yīng)用在實際運行過程中會存在一個訪問高峰期,這個時候內(nèi)存的占用量會是平時的幾倍节榜,因此會多分配幾倍的chunk出來羡玛,而等高峰期過去以后,由于chunk被回收的幾率降低宗苍,內(nèi)存回收的進度就會很慢(因為沒被完全釋放稼稿,所以無法回收)亿遂,內(nèi)存就存在很大的浪費。
為什么是從q050開始嘗試分配呢渺杉,q050是內(nèi)存占用50%~100%的chunk,能夠提高整個應(yīng)用的內(nèi)存使用率挪钓,因為這樣大部分情況下會使用q050的內(nèi)存是越,這樣在內(nèi)存使用不是很多的情況下一些利用率低(<50%)的chunk慢慢就會淘汰出去,最終被回收碌上。
為什么不是從qinit中開始呢倚评,這里的chunk利用率低,但又不會被回收馏予,會形成浪費
q075,q100由于使用率高天梧,分配成功的幾率也會更小,因此放到最后霞丧。如果整個list中都無法分配呢岗,則新建一個chunk,并將其加入到qinit中蛹尝。
Refereneces