Netty內存池之PoolArena詳解

       PoolArena是Netty內存池中的一個核心容器,它的主要做用是對建立的一系列的PoolChunkPoolSubpage進行管理,根據申請的不一樣內存大小將最終的申請動做委託給這兩個子容器進行管理。總體上,PoolArena管理的內存有直接內存和堆內存兩種方式,其是經過子類繼承的方式來實現對不一樣類型的內存的申請與釋放的。本文首先會對PoolArena的總體結構進行介紹,而後會介紹其主要屬性,接着會從源碼的角度對PoolArena申請和釋放內存的過程進行介紹。java

1. 總體結構

       在總體上,PoolArena是對內存申請和釋放的一個抽象,其有兩個子類,結構以下圖所示:數組

image.png

       這裏DirectArenaHeapArenaPoolArena對不一樣類型的內存申請和釋放進行管理的兩個具體的實現,內存的處理工做主要仍是在PoolArena中。從結構上來看,PoolArena中主要包含三部分子內存池:tinySubpagePools,smallSubpagePools和一系列的PoolChunkList。tinySubpagePools和smallSubpagePools都是PoolSubpage的數組,數組長度分別爲32和4;PoolChunkList則主要是一個容器,其內部能夠保存一系列的PoolChunk對象,而且,Netty會根據內存使用率的不一樣,將PoolChunkList分爲不一樣等級的容器。以下是PoolArena在初始狀態時的結構示意圖:緩存

image.png

       關於PoolArena的結構,主要有以下幾點須要說明:多線程

  • 初始狀態時,tinySubpagePools是一個長度爲32的數組,smallSubpagePools是一個長度爲4的數組,其他的對象類型則都是PoolChunkList,只不過PoolArena將其按照其內存使用率分爲qInit->內存使用率爲0~25,q000->內存使用率爲1~50,q025->內存使用率爲25~75,q050->內存使用率爲50~75,q075->內存使用率爲75~100,q100->內存使用率爲100。
  • 初始時,tinySubpagePools和smallSubpagePools數組中的每個元素都是空的,而PoolChunkList內部則沒有保有任何的PoolChunk。從圖中能夠看出,PoolChunkList不只內部保存有PoolChunk對象,並且還有一個指向下一高等級使用率的PoolChunkList的指針。PoolArena這麼設計的緣由在於,若是新建了一個PoolChunk,那麼將其添加到PoolChunkList的時候,只須要將其添加到qInit中便可,其會根據當前PoolChunk的使用率將其依次往下傳遞,以保證將其歸屬到某個其使用率範圍的PoolChunkList中;
  • tinySubpagePools數組中主要是保存大小小於等於496byte的內存,其將0~496byte按照16個字節一個等級拆分紅了31等,而且將其保存在了tinySubpagePools的1~31號位中。須要說明的是,tinySubpagePools中的每個元素中保存的都是一個PoolSubpage鏈表。也就是說,在tinySubpagePools數組中,第1號位中存儲的PoolSubpage維護的內存大小爲16byte,第2號位中存儲的PoolSubpage維護的內存大小爲32byte,第3號位中存儲的PoolSubpage維護的內存大小爲48byte,依次類推,第31號位中存儲的PoolSubpage維護的內存大小爲496byte。關於PoolSubpage的實現原理,讀者能夠閱讀本人前面的文章Netty內存池之PoolSubpage詳解
  • smallSubpagePools數組長度爲4,其維護的內存大小爲496byte~8KB。smallSubpagePools中內存的劃分則是按照2的指數次冪進行的,也就是說其每個元素所維護的PoolSubpage的內存大小都是2的指數次冪,好比第0號位中存儲的PoolSubpage維護的內存大小爲512byte,第1號位爲1024byte,第2號位爲2048byte,第3號位爲4096。須要注意的是,這裏說的維護的內存大小指的是最大內存大小,好比申請的內存大小爲5000 > 4096byte,那麼PoolArena會將其擴展爲8092,而後交由PoolChunk進行申請;
  • 圖中qInit、q000、q02五、q050、q075和q100都是一個PoolChunkList,它們的做用主要是維護大小符合當前使用率大小的PoolChunk。關於PoolChunkList,有以下幾點須要說明:
    • PoolChunkList內部維護了一個PoolChunk的head指針,而PoolChunk自己就是一個單向鏈表,當有新的PoolChunk須要添加到當前PoolChunkList中時,其會將該PoolChunk添加到該鏈表的頭部;
    • PoolChunkList也是一個單項鍊表,如圖中所示,其會根據圖中的順序,在內部維護一個下一等級使用率的PoolChunkList的指針。這樣處理的優勢在於,當須要添加一個PoolChunk到PoolChunkList中時,只須要調用頭結點,也即qInit的add()方法,每一個PoolChunkList都會檢查目標PoolChunk使用率是否符合當前PoolChunkList,若是知足,則添加到當前PoolChunkList維護的PoolChunk鏈表中,若是不知足,則將其交由下一PoolChunkList處理;
    • 圖中每一個PoolChunkList後面都寫了一個數字範圍,這個數字範圍表示的就是當前PoolChunkList所維護的使用率範圍,好比qInit維護的使用率爲0~25%,q000維護的使用率爲1~50%等等;
    • PoolChunkList在維護PoolChunk時,還會對其進行移動操做,好比某個PoolChunk內存使用率爲23%,當前正處於qInit中,在一次內存申請時,從該PoolChunk中申請了5%的內存,此時內存使用率達到了30%,已經不符合當前PoolChunkList(qInit->0~25%)的內存使用率了,此時,PoolChunkList就會將其交由其下一PoolChunkList進行處理,q000就會判斷收到的PoolChunk使用率30%是符合當前PoolChunkList使用率定義的,於是會將其添加到當前PoolChunkList中。
  • 關於內存的申請過程,咱們這裏以申請30byte內存爲例進行講解:
    • 上面的內存劃分中能夠看到,PoolArena內存大小區間爲:tinySubpagePools->低於496byte,smallSubpagePools->512~4096byte,PoolChunkList->8KB~16M。PoolArena首先會判斷目標內存在哪一個內存範圍,30 < 496byte,於是會將其交由tinySubpagePools進行申請;
    • 因爲tinySubpagePools中每一個等級的內存塊劃分是以16byte爲單位的,於是PoolArena會將目標內存擴容到大於其的第一個16的倍數,也就是32(若是申請的內存大小在smallSubpagePools或者PoolChunkList中,那麼其擴容的方式則是查找大於其值的第一個2的指數次冪)。32對應的是tinySubpagePools的下標爲2的PoolSubpage鏈表,這裏就會取tinySubpagePools[2],而後從其頭結點的下一個節點開始判斷是否有足夠的內存(頭結點是不保存內存塊的),若是有則將申請到的內存塊封裝爲一個ByteBuf對象返回;
    • 在初始狀態時,tinySubpagePools數組元素都是空的,於是按照上述步驟將不會申請到對應的內存塊,此時會將申請動做交由PoolChunkList進行。PoolArena首先會依次從qInit、q000、…、q100中申請內存,若是在某一箇中申請到了,則將申請到的內存塊封裝爲一個ByteBuf對象,而且將其返回;
    • 初始時,每個PoolChunkList都沒有可用的PoolChunk對象,此時PoolArena會建立一個新的PoolChunk對象,每一個PoolChunk對象維護的內存大小都是16M。而後內存申請動做就會交由PoolChunk進行,在PoolChunk申請到內存以後,PoolArena就會將建立的這個PoolChunk按照前面將的方式添加到qInit中,qInit會根據該PoolChunk已經使用的內存大小將其移動到對應使用率的PoolChunkList中;
    • 關於PoolChunk申請內存的方式,這裏須要說明的是,咱們申請的是30byte內存,而PoolChunk內存申請最小值爲8KB。於是這裏在PoolChunk申請到8KB內存以後,PoolChunk會將其交由一個PoolSubpage進行維護,而且會設置該PoolSubpage維護的內存塊大小爲32byte,而後根據其維護的內存塊大小,將其放到tinySubpagePools的對應位置,這裏是tinySubpagePools[2]的PoolSubpage鏈表中。放到該鏈表以後,而後再在該PoolSubpage中申請目標內存擴容後的內存,也就是32byte,最後將申請到的內存封裝爲一個ByteBuf對象返回;
  • 能夠看出,PoolArena對內存塊的維護是一個動態的過程,其會根據目標內存塊的大小將其交由不一樣的對象進行處理。這樣作的好處是,因爲內存申請是一個多線程共享的高頻率操做,將內存進行劃分可使得併發處理時可以減少鎖的競爭。以下圖展現了在屢次內存申請以後,PoolArena的一個結構:

image.png

2. PoolArena主要屬性講解

       PoolArena中有很是多的屬性值,用於對PoolSubpage、PookChunk和PoolChunkList進行控制。在閱讀源碼時,若是可以理解這些屬性值的做用,將會極大的加深對Netty內存池的理解。咱們這裏對PoolArena的主要屬性進行介紹:併發

// 該參數指定了tinySubpagePools數組的長度,因爲tinySubpagePools每個元素的內存塊差值爲16,
// 於是數組長度是512/16,也即這裏的512 >>> 4
static final int numTinySubpagePools = 512 >>> 4;
// 記錄了PooledByteBufAllocator的引用
final PooledByteBufAllocator parent;
// PoolChunk底層是一個平衡二叉樹,該參數指定了該二叉樹的深度
private final int maxOrder;
// 該參數指定了PoolChunk中每個葉節點所指代的內存塊的大小
final int pageSize;
// 指定了葉節點大小8KB是2的多少次冪,默認爲13,該字段的主要做用是,在計算目標內存屬於二叉樹的
// 第幾層的時候,能夠藉助於其內存大小相對於pageShifts的差值,從而快速計算其所在層數
final int pageShifts;
// 指定了PoolChunk的初始大小,默認爲16M
final int chunkSize;
// 因爲PoolSubpage的大小爲8KB=8196,於是該字段的值爲
// -8192=>=> 1111 1111 1111 1111 1110 0000 0000 0000
// 這樣在判斷目標內存是否小於8KB時,只須要將目標內存與該數字進行與操做,只要操做結果等於0,
// 就說明目標內存是小於8KB的,這樣就能夠判斷其是應該首先在tinySubpagePools或smallSubpagePools
// 中進行內存申請
final int subpageOverflowMask;
// 該參數指定了smallSubpagePools數組的長度,默認爲4
final int numSmallSubpagePools;
// 指定了直接內存緩存的校準值
final int directMemoryCacheAlignment;
// 指定了直接內存緩存校準值的判斷變量
final int directMemoryCacheAlignmentMask;
// 存儲內存塊小於512byte的PoolSubpage數組,該數組是分層次的,好比其第1層只用於大小爲16byte的
// 內存塊的申請,第2層只用於大小爲32byte的內存塊的申請,……,第31層只用於大小爲496byte的內存塊的申請
private final PoolSubpage<T>[] tinySubpagePools;
// 用於大小在512byte~8KB內存的申請,該數組長度爲4,所申請的內存塊大小爲512byte、1024byte、
// 2048byte和4096byte。
private final PoolSubpage<T>[] smallSubpagePools;
// 用戶維護使用率在50~100%的PoolChunk
private final PoolChunkList<T> q050;
// 用戶維護使用率在25~75%的PoolChunk
private final PoolChunkList<T> q025;
// 用戶維護使用率在1~50%的PoolChunk
private final PoolChunkList<T> q000;
// 用戶維護使用率在0~25%的PoolChunk
private final PoolChunkList<T> qInit;
// 用戶維護使用率在75~100%的PoolChunk
private final PoolChunkList<T> q075;
// 用戶維護使用率爲100%的PoolChunk
private final PoolChunkList<T> q100;
// 記錄了當前PoolArena已經被多少個線程使用了,在每個線程申請新內存的時候,其會找到使用最少的那個
// PoolArena進行內存的申請,這樣能夠減小線程之間的競爭
final AtomicInteger numThreadCaches = new AtomicInteger();

3. 實現源碼講解

3.1 內存申請

       PoolArena對內存申請的控制,主要是按照前面的描述,對其流程進行控制。關於PoolChunk和PoolSubpage對內存申請和釋放的控制,讀者能夠閱讀本人前面的文章:Netty內存池之PoolChunk原理詳解Netty內存池之PoolSubpage詳解。這裏咱們主要在PoolArena層面上對內存的申請進行講解,以下是其allocate()方法的源碼:this

PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
  // 這裏newByteBuf()方法將會建立一個PooledByteBuf對象,可是該對象是未經初始化的,
  // 也就是說其內部的ByteBuffer和readerIndex,writerIndex等參數都是默認值
  PooledByteBuf<T> buf = newByteBuf(maxCapacity);
  // 使用對應的方式爲建立的ByteBuf初始化相關內存數據,咱們這裏是以DirectArena進行講解,於是這裏
  // 是經過其allocate()方法申請內存
  allocate(cache, buf, reqCapacity);
  return buf;
}

       上述方法主要是一個入口方法,首先建立一個屬性都是默認值的ByteBuf對象,而後將真正的申請動做交由allocate()方法進行:.net

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
  // 這裏normalizeCapacity()方法的主要做用是對目標容量進行規整操做,主要規則以下:
  // 1. 若是目標容量小於16字節,則返回16;
  // 2. 若是目標容量大於16字節,小於512字節,則以16字節爲單位,返回大於目標字節數的第一個16字節的倍數。
  //    好比申請的100字節,那麼大於100的16的倍數是112,於是返回112個字節
  // 3. 若是目標容量大於512字節,則返回大於目標容量的第一個2的指數冪。
  //    好比申請的1000字節,那麼返回的將是1024
  final int normCapacity = normalizeCapacity(reqCapacity);
  // 判斷目標容量是否小於8KB,小於8KB則使用tiny或small的方式申請內存
  if (isTinyOrSmall(normCapacity)) {
    int tableIdx;
    PoolSubpage<T>[] table;
    boolean tiny = isTiny(normCapacity);  // 判斷目標容量是否小於512字節,小於512字節的爲tiny類型的
    if (tiny) {
      // 這裏首先從當前線程的緩存中嘗試申請內存,若是申請到了,則直接返回,該方法中會使用申請到的
      // 內存對ByteBuf對象進行初始化
      if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
        return;
      }
      
      // 若是沒法從當前線程緩存中申請到內存,則嘗試從tinySubpagePools中申請,這裏tinyIdx()方法
      // 就是計算目標內存是在tinySubpagePools數組中的第幾號元素中的
      tableIdx = tinyIdx(normCapacity);
      table = tinySubpagePools;
    } else {
      // 若是目標內存在512byte~8KB之間,則嘗試從smallSubpagePools中申請內存。這裏首先從
      // 當前線程的緩存中申請small級別的內存,若是申請到了,則直接返回
      if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
        return;
      }
      
      // 若是沒法從當前線程的緩存中申請到small級別的內存,則嘗試從smallSubpagePools中申請。
      // 這裏smallIdx()方法就是計算目標內存塊是在smallSubpagePools中的第幾號元素中的
      tableIdx = smallIdx(normCapacity);
      table = smallSubpagePools;
    }

    // 獲取目標元素的頭結點
    final PoolSubpage<T> head = table[tableIdx];

    // 這裏須要注意的是,因爲對head進行了加鎖,而在同步代碼塊中判斷了s != head,
    // 也就是說PoolSubpage鏈表中是存在未使用的PoolSubpage的,由於若是該節點已經用完了,
    // 其是會被移除當前鏈表的。也就是說只要s != head,那麼這裏的allocate()方法
    // 就必定可以申請到所須要的內存塊
    synchronized (head) {
      final PoolSubpage<T> s = head.next;
      // s != head就證實當前PoolSubpage鏈表中存在可用的PoolSubpage,而且必定可以申請到內存,
      // 由於已經耗盡的PoolSubpage是會從鏈表中移除的
      if (s != head) {
        // 從PoolSubpage中申請內存
        long handle = s.allocate();
        // 經過申請的內存對ByteBuf進行初始化
        s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
        // 對tiny類型的申請數進行更新
        incTinySmallAllocation(tiny);
        return;
      }
    }
    
    synchronized (this) {
      // 走到這裏,說明目標PoolSubpage鏈表中沒法申請到目標內存塊,於是就嘗試從PoolChunk中申請
      allocateNormal(buf, reqCapacity, normCapacity);
    }

    // 對tiny類型的申請數進行更新
    incTinySmallAllocation(tiny);
    return;
  }
  
  // 走到這裏說明目標內存是大於8KB的,那麼就判斷目標內存是否大於16M,若是大於16M,
  // 則不使用內存池對其進行管理,若是小於16M,則到PoolChunkList中進行內存申請
  if (normCapacity <= chunkSize) {
    // 小於16M,首先到當前線程的緩存中申請,若是申請到了則直接返回,若是沒有申請到,
    // 則到PoolChunkList中進行申請
    if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
      return;
    }
    synchronized (this) {
      // 在當前線程的緩存中沒法申請到足夠的內存,於是嘗試到PoolChunkList中申請內存
      allocateNormal(buf, reqCapacity, normCapacity);
      ++allocationsNormal;
    }
  } else {
    // 對於大於16M的內存,Netty不會對其進行維護,而是直接申請,而後返回給用戶使用
    allocateHuge(buf, reqCapacity);
  }
}

       上述代碼就是PoolArena申請目標內存塊的主要流程,首先會判斷目標內存是在哪一個內存層級的,好比tiny、small或者normal,而後根據目標層級的分配方式對目標內存進行擴容。接着首先會嘗試從當前線程的緩存中申請目標內存,若是可以申請到,則直接返回,若是不能申請到,則在當前層級中申請。對於tiny和small層級的內存申請,若是沒法申請到,則會將申請動做交由PoolChunkList進行。這裏咱們主要看一下PoolArena是如何在PoolChunkList中申請內存的,以下是allocateNormal()的源碼:線程

private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
  // 將申請動做按照q050->q025->q000->qInit->q075的順序依次交由各個PoolChunkList進行處理,
  // 若是在對應的PoolChunkList中申請到了內存,則直接返回
  if (q050.allocate(buf, reqCapacity, normCapacity)
      || q025.allocate(buf, reqCapacity, normCapacity)
      || q000.allocate(buf, reqCapacity, normCapacity)
      || qInit.allocate(buf, reqCapacity, normCapacity)
      || q075.allocate(buf, reqCapacity, normCapacity)) {
    return;
  }

  // 因爲在目標PoolChunkList中沒法申請到內存,於是這裏直接建立一個PoolChunk,
  // 而後在該PoolChunk中申請目標內存,最後將該PoolChunk添加到qInit中
  PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
  boolean success = c.allocate(buf, reqCapacity, normCapacity);
  qInit.add(c);
}

       這裏申請過程比較簡單,首先是按照必定的順序分別在各個PoolChunkList中申請內存,若是申請到了,則直接返回,若是沒申請到,則建立一個PoolChunk進行申請。這裏須要說明的是,在PoolChunkList中申請內存時,本質上仍是將申請動做交由其內部的PoolChunk進行申請,若是申請到了,其還會判斷當前PoolChunk的內存使用率是否超過了當前PoolChunkList的閾值,若是超過了,則會將其移動到下一PoolChunkList中。設計

3.2 內存釋放

       對於內存的釋放,PoolArena主要是分爲兩種狀況,即池化和非池化,若是是非池化,則會直接銷燬目標內存塊,若是是池化的,則會將其添加到當前線程的緩存中。以下是free()方法的源碼:指針

void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity,
     PoolThreadCache cache) {
  // 若是是非池化的,則直接銷燬目標內存塊,而且更新相關的數據
  if (chunk.unpooled) {
    int size = chunk.chunkSize();
    destroyChunk(chunk);
    activeBytesHuge.add(-size);
    deallocationsHuge.increment();
  } else {
    // 若是是池化的,首先判斷其是哪一種類型的,即tiny,small或者normal,
    // 而後將其交由當前線程的緩存進行處理,若是添加成功,則直接返回
    SizeClass sizeClass = sizeClass(normCapacity);
    if (cache != null && cache.add(this, chunk, nioBuffer, handle,
          normCapacity, sizeClass)) {
      return;
    }

    // 若是當前線程的緩存已滿,則將目標內存塊返還給公共內存塊進行處理
    freeChunk(chunk, handle, sizeClass, nioBuffer);
  }
}

4. 小結

       本文首先對PoolArena的總體結構進行了講解,而且講解了PoolArena是如何控制內存申請流轉的,而後介紹了PoolArena中各個屬性的做用,最後從源碼的角度講解了PoolArena是如何控制內存的申請的。

相關文章
相關標籤/搜索