1. 程式人生 > >Netty記憶體池值PoolSubpage詳解

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的實現原理。

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;

       對於各個屬性的初始化,我們可以通過建構函式進行講解,如下是其建構函式原始碼:

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中申請記憶體的:

// 對於allocate()方法,其沒有傳入任何引數是因為當前PoolSubpage所能申請的記憶體塊大小在構造方法中
// 已經通過elemSize指定了。
// 當前方法返回的是一個long型整數,這裡是將要申請的記憶體塊使用了一個long型變數進行表徵了。由於一個記憶體塊
// 是否使用是通過一個long型整數表示的,因而,如果想要表徵當前申請到的記憶體塊是這個long型整數中的哪一位,
// 只需要一個最大為63的整數即可(long最多為64位),這隻需要long型數的低6位就可以表示,由於我們使用的是一個
// long型陣列,因而還需要記錄當前是在陣列中第幾個元素,由於陣列長度最多為8,因而對於返回值的7~10位則是記錄
// 了當前申請的記憶體塊是在bitmap陣列的第幾個元素中。總結來說,返回值的long型數的高32位中的低6位
// 記錄了當前申請的是是bitmap中某個long的第幾個位置的記憶體塊,而高32位的7~10位則記錄了申請的是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~10號位置上

  // 對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型元素中是否有可用的記憶體塊。

       下面我們繼續看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的實現原理,然後講解了其是如何控制記憶體的申請和釋放的,最後從原始碼層面對其申請和釋放記憶體的行