這是一篇關於內存管理算法的文章,對於Java開發者而言這個話題比較遙遠。 雖然咱們平常開發中一直在跟內存打交道,但不多關注過內存管理的具體細節,畢竟JVM已經作得很好了。 然而在高併發場景下,程序運行過程當中產生的大量內存對象,會形成必定的GC負擔,這直接影響着程序運行性能。若是能緩解一部分GC壓力,節省下來的系統資源便會對性能有顯著的提高,由此便衍生出了池技術。java
本次咱們分享的內存池技術主要用於提高網絡通訊的I/O能力,固然該技術也可用於本地磁盤I/O。比較常見的內存管理算法有如下幾種:算法
首次適應算法(First-Fit)數組
從空閒分區表的第一個表目起查找該表,把最早可以知足要求的空閒區分配給做業,這種方法目的在於減小查找時間。爲適應這種算法,空閒分區表(空閒區鏈)中的空閒分區要按地址由低到高進行排序。該算法優先使用低址部分空閒區,在低址空間形成許多小的空閒區,在高地址空間保留大的空閒區。網絡
優勢多線程
該算法傾向於優先利用內存中低址部分的空閒分區,從而保留了高址部分的大空閒區,這爲之後到達的大做業分配大的內存空間創造了條件。併發
缺點socket
低址部分不斷被劃分,會留下許多難以利用的,很小的空閒分區,稱爲碎片。而每次查找又都是從低址部分開始的,這無疑又會增長查找可用空閒分區時的開銷。高併發
最佳適應算法(Best-Fit)性能
從所有空閒區中找出能知足做業要求的、且大小最小的空閒分區,這種方法能使碎片儘可能小。爲適應此算法,空閒分區表(空閒區鏈)中的空閒分區要按從小到大進行排序,自表頭開始查找到第一個知足要求的自由分區分配。該算法保留大的空閒區,但形成許多小的空閒區。this
最差適應算法(Worst-Fit)
它從所有空閒區中找出能知足做業要求的、且大小最大的空閒分區,從而使鏈表中的結點大小趨於均勻,適用於請求分配的內存大小範圍較窄的系統。爲適應此算法,空閒分區表(空閒區鏈)中的空閒分區要按大小從大到小進行排序,自表頭開始查找到第一個知足要求的自由分區分配。該算法保留小的空閒區,儘可能減小小的碎片產生。
這些算法各有優劣,本次咱們只分享首次適應算法,smart-socket中正是應用了該算法實現的高性能通訊。
接下來咱們經過幾個步驟來演示內存申請、釋放的過程,以及在此過程當中如何致使內存碎片化的產生。
初始狀態內存容量爲15。
ABCDE前後申請特定大小的內存塊:一、二、三、四、5,此時內存池中已無可用空間。
B、D釋放內存,內存池中出現兩塊不相鄰的內存塊。後續再次申請內存即可從這兩塊不相鄰的內存塊中挑選可用空間進行分配。
F申請1字節,G申請2字節。按First-Fit算法,會優先從低位查找可用內存塊。當F申請到第2位內存塊後,緊鄰的3號內存塊便再也不知足G所需的2字節,因此只能從7~10號內存塊中申請2字節。若是內存塊小到沒法知足應用所需,便成了內存碎片。
A、C、E回收內存,內存池中還原出了大片可用區域。如若F、G也釋放內存,則次內存池便恢復如初。
availableBuffers有序存儲了內存池申請/釋放過程當中產生的內存塊。低地址內存塊存儲於隊列頭部,高地址存於隊列尾部。
申請內存時遍歷內存塊隊列,查找容量足夠的內存塊。
若是內存塊容量恰好符合申請所需大小,則從隊列中移除該內存塊並返回。
若是內存容量大於申請所需大小,則對該內存塊進行拆分。只返回所需大小的內存塊,剩餘部分存留於隊列中。
若無可用內存塊,則申請失敗,此時只能建立臨時內存塊。
public VirtualBuffer allocate(final int size) { lock.lock(); try { Iterator<VirtualBuffer> iterator = availableBuffers.iterator(); VirtualBuffer bufferChunk; while (iterator.hasNext()) { VirtualBuffer freeChunk = iterator.next(); final int remaining = freeChunk.getParentLimit() - freeChunk.getParentPosition(); if (remaining < size) { continue; } if (remaining == size) { iterator.remove(); buffer.limit(freeChunk.getParentLimit()); buffer.position(freeChunk.getParentPosition()); freeChunk.buffer(buffer.slice()); bufferChunk = freeChunk; } else { buffer.limit(freeChunk.getParentPosition() + size); buffer.position(freeChunk.getParentPosition()); bufferChunk = new VirtualBuffer(this, buffer.slice(), buffer.position(), buffer.limit()); freeChunk.setParentPosition(buffer.limit()); } return bufferChunk; } } finally { lock.unlock(); } return new VirtualBuffer(null, allocate0(size, false), 0, 0); }
使用完畢的內存塊須要主動釋放回收,以供下次繼續使用。釋放的過程主要作到兩點:
找到被釋放內存塊在內存隊列中的正確點位。
被釋放內存塊所處的點位若能與先後相鄰內存塊造成連續內存塊,則合併內存塊;反之,則直接放入隊列中便可。
private void clean0(VirtualBuffer cleanBuffer) { int index = 0; Iterator<VirtualBuffer> iterator = availableBuffers.iterator(); while (iterator.hasNext()) { VirtualBuffer freeBuffer = iterator.next(); //cleanBuffer在freeBuffer以前而且造成連續塊 if (freeBuffer.getParentPosition() == cleanBuffer.getParentLimit()) { freeBuffer.setParentPosition(cleanBuffer.getParentPosition()); return; } //cleanBuffer與freeBuffer以後並造成連續塊 if (freeBuffer.getParentLimit() == cleanBuffer.getParentPosition()) { freeBuffer.setParentLimit(cleanBuffer.getParentLimit()); //判斷後一個是否連續 if (iterator.hasNext()) { VirtualBuffer next = iterator.next(); if (next.getParentPosition() == freeBuffer.getParentLimit()) { freeBuffer.setParentLimit(next.getParentLimit()); iterator.remove(); } else if (next.getParentPosition() < freeBuffer.getParentLimit()) { throw new IllegalStateException(""); } } return; } if (freeBuffer.getParentPosition() > cleanBuffer.getParentLimit()) { availableBuffers.add(index, cleanBuffer); return; } index++; } availableBuffers.add(cleanBuffer); }
完整代碼參閱smart-socket項目中的BufferPage.java
內存申請/釋放在實際應用中還有一個沒法迴避的問題,那就是併發。如何才能在高併發場景下保證內存池依舊能高效穩定的提供申請與釋放服務? 爲了不多線程併發申請致使某塊內存區域被屢次分配,必需要對申請的過程加同步鎖控制,內存釋放的過程亦是如此。
可一旦加上同步鎖,內存的申請、釋放性能必然受到影響。最爲理想的狀態是每個CPU綁定着獨立的內存池對象, 運行時便不存在多個CPU對同一個內存池對象進行申請/釋放操做,這樣即可實現無鎖化。
惋惜CPU綁定內存池的想法沒法實現,只能作到線程級的隔離,採用ThreadLocal即可。只不過此方式如若使用不當可能出現內存泄露,以及內存池資源利用率不高等狀況。 爲此,推薦的作法是採用數組的方式來維護多個內存池對象,使用時經過某種均衡策略將內存池對象分配給任務做業。 雖然不能杜絕鎖競爭的狀況發生,但在必定程度上仍是能夠下降鎖機率的。