Netty內存池之PoolSubpage詳解

       在Netty內存池中,內存大小在8KB~16M的內存是由PoolChunk維護的,小於8KB的內存則是由PoolSubpage來維護的。而對於低於8KB的內存,Netty也是將其分紅了兩種狀況0~496byte和512byte~8KB。其中,0~496byte的內存是由一個名稱爲tinySubpagePools的PoolSubpage的數組維護的,512byte~8KB的內存則是由名稱爲smallSubpagePools的PoolSubpage數組來維護的。本文首先會對tinySubpagePools和smallSubpagePools的總體結構進行說明,而後會講解Netty是如何僅僅經過抽象出一種PoolSubpage的數據結構來實現對兩種不一樣的內存區間的管理的,最後本文會從PoolSubpage的源碼的角度來說解PoolSubpage的實現原理。java

1. tinySubpagePools和smallSubpagePools總體結構

       這裏咱們直接查看這兩個PoolSubpage數組的結構:算法

image.png

image.png

  • tinySubpagePools和smallSubpagePools在結構上都是由一個數組來實現的,只是tinySubpagePools的數組長度爲32,可是其真正使用的只有其下標在1~31內的節點。而smallSubpagePools的數組長度爲4,其每一個節點都會使用;
  • 在存儲數據內存的劃分上,圖中,咱們能夠看到,兩個數組的每一個節點都是一個PoolSubpage的單向鏈表,而節點前面咱們都使用了一個數字進行標註。這個數字的意思是,這個節點所對應的鏈表所可以申請的內存最大值,這樣就能夠達到將不一樣大小的內存申請進行了劃分,而且加鎖的時候能夠減少鎖的粒度,從而減少競爭。這裏好比咱們申請8byte的內存,那麼其就會到tinySubpagePools的下標爲1的鏈表中進行申請,須要注意的是,若是該下標位置的鏈表是空的,那麼就會建立一個,可是必定會保證是在該下標處進行申請;
  • tinySubpagePools和smallSubpagePools的最大區別在於二者對於內存的劃分。圖中咱們能夠看到,tinySubpagePools的每一個節點所指代的內存相差16byte,而smallSubpagePools的內存則是2的指數次冪;
  • 在對內存的管理上,這裏每個PoolSubpage也都是維護的一個內存池,它們的大小永遠都是8KB。這裏好比tinySubpagePools的第1號位的每個PoolSubpage,其可以申請的內存最大爲16byte,因爲每個PoolSubpage的大小都爲8KB,於是其鏈表中每一個PoolSubpage都維護了8192 / 16 = 512個內存塊;由好比smallSubpagePools的第2號位的每個PoolSubpage,其可以申請的內存最大爲2048byte,於是其鏈表中每個PoolSubpage都維護了8192 / 2048 = 4個內存塊;
  • 在進行內存申請時,用戶會傳入一個其所但願的內存大小,但實際獲取的大小,Netty都會進行擴容,這裏咱們以50byte內存的申請爲例進行講解:
    • 首先Netty會判斷目標內存,這裏爲50byte,是否小於8KB,只有小於8KB的內存纔會交由tinySubpagePools和smallSubpagePools進行申請;進行了如此判斷以後,Netty還會判斷其是否小於512byte,從而判斷其是應該從tinySubpagePools仍是從smallSubpagePools中進行申請,這裏50小於512,於是是從tinySubpagePools中進行申請;
    • 將目標內存進行擴容,由於已經知道其是從tinySubpagePools中進行申請,因爲tinySubpagePools中的內存階梯是16的倍數,於是會將目標內存擴容爲大於其值的第一個16的倍數,這裏也就是64。若是目標內存是在smallSubpagePools中,那麼就會將其擴容爲大於該值的第一個2的指數次冪;
    • 根據擴容後的大小計算出其在數組中的位置,這裏就是64 / 16 = 4(在Netty源碼中是直接將目標內存向右移動4位,即64 >>> 4,這樣也能達到除以16的目的);
    • 在目標鏈表中找到第一個PoolSubpage,從其剩餘的內存中劃分目標內存塊,這裏須要注意的是,第一個PoolSubpage中是必定會存在可劃分的內存塊的,由於若是鏈表中某個PoolSubpage中沒有剩餘的可劃份內存塊時,其將會被從當前鏈表中移除。關於PoolSubpage內存塊的劃分,後面會進行詳細講解。

2. PoolSubpage實現原理講解

       對於PoolSubpage的實現原理,其內部本質上是使用一個位圖索引來表徵某個內存塊是否已經被佔用了的。前面咱們講到,每一個PoolSubpage的總內存大小都是8192byte,這裏咱們以tinySubpagePools的第1號位的大小爲16字節的PoolSubpage爲例進行講解(其實從這裏就能夠看出,前面咱們圖中數組前面的數字就是表示當前節點鏈表中PoolSubpage所劃分的內存塊的大小)。數組

       因爲每一個內存塊大小爲16字節,而總大小爲8192字節,於是總會有8192 / 16 = 512個內存塊。爲了對這些內存塊進行標記,那麼就須要一個長度爲512的二進制位圖索引進行表徵。Netty並無使用jdk提供的BitMap這個類,而是使用了一個long型的數組。因爲一個long佔用的字節數爲64,於是總共須要512 / 64 = 8個long型數字來表示。這也就是PoolSubpage中的long[] bitmap屬性的做用。下圖表示了PoolSubpage使用位圖索引表示每一個內存塊是否被使用的一個示意圖:數據結構

image.png

       這裏須要說明的是,咱們這裏是以每一個內存塊的大小爲16爲例進行講解的,而16是PoolSubpage所能維護的最小內存塊,對於其餘大小的內存塊,其個數是比512要小的,可是PoolSubpage始終會聲明一個長度爲8的long型數組,而且聲明一個bitmapLength來記錄當前PoolSubpage中有幾個long是用於標誌內存塊使用狀況的。函數

3. PoolSubpage源碼講解

       對於PoolSubpage的實現原理,咱們這裏首先對其各個屬性進行講解:優化

// 記錄當前PoolSubpage的8KB內存塊是從哪個PoolChunk中申請到的
final PoolChunk<T> chunk;
// 當前PoolSubpage申請的8KB內存在PoolChunk中memoryMap中的下標索引
private final int memoryMapIdx;
// 當前PoolSubpage佔用的8KB內存在PoolChunk中相對於葉節點的起始點的偏移量
private final int runOffset;
// 當前PoolSubpage的頁大小,默認爲8KB
private final int pageSize;
// 存儲當前PoolSubpage中各個內存塊的使用狀況
private final long[] bitmap;

PoolSubpage<T> prev;	// 指向前置節點的指針
PoolSubpage<T> next;	// 指向後置節點的指針

boolean doNotDestroy;	// 表徵當前PoolSubpage是否已經被銷燬了
int elemSize;	// 表徵每一個內存塊的大小,好比咱們這裏的就是16
private int maxNumElems;	// 記錄內存塊的總個數
private int bitmapLength;	// 記錄總共可以使用的bitmap數組的元素的個數
// 記錄下一個可用的節點,初始爲0,只要在該PoolSubpage中申請過一次內存,就會更新爲-1,
// 而後一直不會發生變化
private int nextAvail;
// 剩餘可用的內存塊的個數
private int numAvail;

       對於各個屬性的初始化,咱們能夠經過構造函數進行講解,以下是其構造函數源碼:this

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, 
    int pageSize, int elemSize) {
  this.chunk = chunk;
  this.memoryMapIdx = memoryMapIdx;
  this.runOffset = runOffset;
  this.pageSize = pageSize;	// 初始化當前PoolSubpage總內存大小,默認爲8KB
  // 計算bitmap長度,這裏pageSize >>> 10其實就是將pageSize / 1024,獲得的是8,
  // 從這裏就能夠看出,不管內存塊的大小是多少,這裏的bitmap長度永遠是8,由於pageSize始終是不變的
  bitmap = new long[pageSize >>> 10];
  // 對其他的屬性進行初始化
  init(head, elemSize);
}

void init(PoolSubpage<T> head, int elemSize) {
  doNotDestroy = true;
  // elemSize記錄了當前內存塊的大小
  this.elemSize = elemSize;
  if (elemSize != 0) {
    // 初始時,numAvail記錄了可以使用的內存塊個數,其個數能夠經過pageSize / elemSize計算,
    // 咱們這裏就是8192 / 16 = 512。maxNumElems指的是最大可以使用的內存塊個數,
    // 初始時其是與可用內存塊個數一致的。
    maxNumElems = numAvail = pageSize / elemSize;
    nextAvail = 0;	// 初始時,nextAvail是0
    // 這裏bitmapLength記錄了可使用的bitmap的元素個數,這是由於,咱們示例使用的內存塊大小是16,
    // 於是其總共有512個內存塊,須要8個long才能記錄,可是對於一些大小更大的內存塊,好比smallSubpagePools
    // 中內存塊爲1024字節大小,那麼其只有8192 / 1024 = 8個內存塊,也就只須要一個long就能夠表示,
    // 此時bitmapLength就是8。
    // 這裏的計算方式應該是bitmapLength = maxNumElems / 64,由於64是一個long的總字節數,
    // 可是Netty將其進行了優化,也就是這裏的maxNumElems >>> 6,這是由於2的6次方正好爲64
    bitmapLength = maxNumElems >>> 6;
    // 這裏(maxNumElems & 63) != 0就是判斷元素個數是否小於64,若是小於,則須要將bitmapLegth加一。
    // 這是由於若是其小於64,前面一步的位移操做結果爲0,但其仍是須要一個long來記錄
    if ((maxNumElems & 63) != 0) {
      bitmapLength++;
    }

    // 對bitmap數組的值進行初始化
    for (int i = 0; i < bitmapLength; i++) {
      bitmap[i] = 0;
    }
  }
  
  // 將當前PoolSubpage添加到PoolSubpage的鏈表中,也就是最開始圖中的鏈表
  addToPool(head);
}

       這裏對於PoolSubpage的初始化主要是對bitmap、numAvail、bitmapLength的初始化,下面咱們看看其是如何經過這些屬性來從PoolSubpage中申請內存的:3d

// 對於allocate()方法,其沒有傳入任何參數是由於當前PoolSubpage所能申請的內存塊大小在構造方法中
// 已經經過elemSize指定了。
// 當前方法返回的是一個long型整數,這裏是將要申請的內存塊使用了一個long型變量進行表徵了。因爲一個內存塊
// 是否使用是經過一個long型整數表示的,於是,若是想要表徵當前申請到的內存塊是這個long型整數中的哪一位,
// 只須要一個最大爲63的整數便可(long最多爲64位),這隻須要long型數的低6位就能夠表示,因爲咱們使用的是一個
// long型數組,於是還須要記錄當前是在數組中第幾個元素,因爲數組長度最多爲8,於是對於返回值的7~9位則是記錄
// 了當前申請的內存塊是在bitmap數組的第幾個元素中。總結來講,返回值的long型數的高32位中的低6位
// 記錄了當前申請的是是bitmap中某個long的第幾個位置的內存塊,而高32位的7~9位則記錄了申請的是bitmap數組
// 中的第幾號元素。
// 這裏說返回值的高32位是由於其低32位記錄了當前8KB內存塊是在PoolChunk中具體的位置,關於這一塊的算法
// 讀者能夠閱讀本人前面對PoolChunk進行講解的文章
long allocate() {
  // 若是elemSize爲0,則直接返回0
  if (elemSize == 0) {
    return toHandle(0);
  }

  // 若是當前PoolSubpage沒有可用的元素,或者已經被銷燬了,則返回-1
  if (numAvail == 0 || !doNotDestroy) {
    return -1;
  }

  // 計算下一個可用的內存塊的位置
  final int bitmapIdx = getNextAvail();
  int q = bitmapIdx >>> 6;	// 獲取該內存塊是bitmap數組中的第幾號元素
  int r = bitmapIdx & 63;		// 獲取該內存塊是bitmap數組中q號位元素的第多少位
  bitmap[q] |= 1L << r;	// 將bitmap數組中q號元素的目標內存塊位置標記爲1,表示已經使用

  // 若是當前PoolSubpage中可用的內存塊爲0,則將其從鏈表中移除
  if (--numAvail == 0) {
    removeFromPool();
  }

  // 將獲得的bitmapIdx放到返回值的高32位中
  return toHandle(bitmapIdx);
}

       這裏allocate()方法首先會計算下一個可用的內存塊的位置,而後將該位置標記爲1,最後將獲得的位置數據放到返回值的高32位中。這裏咱們繼續看其是如何計算下一個可用的位置的,以下是getNextAvail()的源碼:指針

private int getNextAvail() {
  int nextAvail = this.nextAvail;
  // 若是是第一次嘗試獲取數據,則直接返回bitmap第0號位置的long的第0號元素,
  // 這裏nextAvail初始時爲0,在第一次申請以後就會變爲-1,後面將再也不發生變化,
  // 經過該變量能夠判斷是不是第一次嘗試申請內存
  if (nextAvail >= 0) {
    this.nextAvail = -1;
    return nextAvail;
  }
  
  // 若是不是第一次申請內存,則在bitmap中進行遍歷獲取
  return findNextAvail();
}

private int findNextAvail() {
  final long[] bitmap = this.bitmap;
  final int bitmapLength = this.bitmapLength;
  // 這裏的基本思路就是對bitmap數組進行遍歷,首先判斷其是否有未使用的內存是否所有被使用過
  // 若是有未被使用的內存,那麼就在該元素中找可用的內存塊的位置
  for (int i = 0; i < bitmapLength; i++) {
    long bits = bitmap[i];
    if (~bits != 0) {	// 判斷當前long型元素中是否有可用內存塊
      return findNextAvail0(i, bits);
    }
  }
  return -1;
}

// 入參中i表示當前是bitmap數組中的第幾個元素,bits表示該元素的值
private int findNextAvail0(int i, long bits) {
  final int maxNumElems = this.maxNumElems;
  final int baseVal = i << 6;	// 這裏baseVal就是將當前是第幾號元素放到返回值的第7~9號位置上

  // 對bits的0~63號位置進行遍歷,判斷其是否爲0,爲0表示該位置是可用內存塊,從而將位置數據
  // 和baseVal進行或操做,從而獲得一個表徵目標內存塊位置的整型數據
  for (int j = 0; j < 64; j++) {
    if ((bits & 1) == 0) {	// 判斷當前位置是否爲0,若是爲0,則表示是目標內存塊
      int val = baseVal | j;	// 將內存快的位置數據和其位置j進行或操做,從而獲得返回值
      if (val < maxNumElems) {
        return val;
      } else {
        break;
      }
    }
    bits >>>= 1;	// 將bits不斷的向右移位,以找到第一個爲0的位置
  }
  return -1;
}

       上面的查找過程很是的簡單,其原理起始就是對bitmap數組進行遍歷,首先判斷當前元素是否有可用的內存塊,若是有,則在該long型元素中進行遍歷,找到第一個可用的內存塊,最後將表徵該內存塊位置的整型數據返回。這裏須要說明的是,上面判斷bitmap中某個元素是否有可用內存塊是使用的是~bits != 0來計算的,該算法的原理起始就是,若是一個long中全部的內存塊都被申請了,那麼這個long必然全部的位都爲1,從總體上,這個long型數據的值就爲-1,而將其取反~bits以後,值確定就變爲了0,於是這裏只須要判斷其取反以後是否等於0便可判斷當前long型元素中是否有可用的內存塊。code

       下面咱們繼續看PoolSubpage是如何對內存進行釋放的,以下是free()方法的源碼:

boolean free(PoolSubpage<T> head, int bitmapIdx) {
  if (elemSize == 0) {
    return true;
  }
  
  // 獲取當前須要釋放的內存塊是在bitmap中的第幾號元素
  int q = bitmapIdx >>> 6;
  // 獲取當前釋放的內存塊是在q號元素的long型數的第幾位
  int r = bitmapIdx & 63;
  // 將目標位置標記爲0,表示可以使用狀態
  bitmap[q] ^= 1L << r;

  // 設置下一個可以使用的數據
  setNextAvail(bitmapIdx);

  // numAvail若是等於0,表示以前已經被移除鏈表了,於是這裏釋放後須要將其添加到鏈表中
  if (numAvail++ == 0) {
    addToPool(head);
    return true;
  }

  // 若是可用的數量小於最大數量,則表示其仍是在鏈表中,於是直接返回true
  if (numAvail != maxNumElems) {
    return true;
  } else {
    // else分支表示當前PoolSubpage中沒有任何一個內存塊被佔用了
    // 這裏若是當前PoolSubpage的前置節點和後置節點相等,這表示其都是默認的head節點,也就是
    // 說當前鏈表中只有一個可用於內存申請的節點,也就是當前PoolSubpage,這裏就不會將其移除
    if (prev == next) {
      return true;
    }

    // 若是有多個節點,則將當前PoolSubpage移除
    doNotDestroy = false;
    removeFromPool();
    return false;
  }
}

       能夠看到,對於free()操做,主要是將目標位置標記爲0,而後設置相關屬性,而且判斷是否須要將當前PoolSubpage添加到鏈表中或者從鏈表移除。

4. 小結

       本文首先講解了PoolSubpage的實現原理,而後講解了其是如何控制內存的申請和釋放的,最後從源碼層面對其申請和釋放內存的行爲進行了講解。

相關文章
相關標籤/搜索