摘要: 在Netty中,通常會有多個IO線程獨立工作,基於NioEventLoop的實現,每個IO線程負責輪詢單獨的Selector實例來檢索IO事件,當IO事件來臨的時候,IO線程開始處理IO事件。最常見的IO事件即讀寫事件,那么這個時候就會涉及到IO線程對數據的讀寫問題,具體到NIO方面即從內核緩沖區讀取數據到用戶緩沖區或者從用戶緩沖區將數據寫到內核緩沖區。NIO提供了兩種Buffer作為緩沖區,即DirectBuffer和HeapBuffer。這篇文章主要在介紹兩種緩沖區的基礎之上再介紹Netty基於ThreadLocal的內存池技術的實現原理與應用,並給出一個簡單維度的測試數據。
在Netty中,通常會有多個IO線程獨立工作,基於NioEventLoop的實現,每個IO線程負責輪詢單獨的Selector實例來檢索IO事件,當IO事件來臨的時候,IO線程開始處理IO事件。最常見的IO事件即讀寫事件,那么這個時候就會涉及到IO線程對數據的讀寫問題,具體到NIO方面即從內核緩沖區讀取數據到用戶緩沖區或者從用戶緩沖區將數據寫到內核緩沖區。NIO提供了兩種Buffer作為緩沖區,即DirectBuffer和HeapBuffer。這篇文章主要在介紹兩種緩沖區的基礎之上再介紹Netty基於ThreadLocal的內存池技術的實現原理與應用,並給出一個簡單維度的測試數據。
DirectBuffer和HeapBuffer
DirectBuffer顧名思義是分配在直接內存(Direct Memory)上面的內存區域,直接內存不是JVM Runtime數據區的一部分,也不是JAVA虛擬機規范中定義的內存區域,但是這部分內存也被頻繁的使用。在JDK1.4版本開始NIO引入的Channel與Buffer的IO方式使得我們可以使用native接口來在直接內存上分配內存,並用JVM堆內存上的一個引用來進行操作,當JVM堆內存上的引用被回收之后,這塊直接內存才會被操作系統回收。HeapBuffer即分配在JVM堆內存區域的緩沖區,我們可以簡單理解為HeapBuffer就是byte[]數組的一種封裝形式。
基於HeapBuffer的IO寫流程通常是先要在直接內存上分配一個臨時的緩沖區,然后將數據copy到直接內存,然后再將這塊直接內存上的數據發送到IO設備的緩沖區,最后銷毀臨時直接內存區域。而基於HeapBuffer的IO讀流程也類似。使用DirectBuffer之后,避免了JVM堆內存和直接內存之間數據來回復制,在一些應用場景中性能有顯著的提高。除了避免多次拷貝之外直接內存的另一個好處就是訪問速度快,這跟JVM的對象訪問方式有關。
DirectBuffer的缺點在於直接內存的分配與回收代價相對比較大,因此DirectBuffer適用於緩沖區可以重復使用的場景。
Netty中的Buffers
在Netty中,緩沖區有兩種形式即HeapBuffer和DirectBuffer。Netty對於他們都進行了池化:
其中對應堆內存和直接內存的池化實現分別是PooledHeapByteBuf和PooledDirectByteBuf,在各自的實現中都維護着一個Recycler,這個Recycler就是本文關注的重點,也是Netty輕量級內存池技術的核心實現。
Recycler及內部組件
Recycler是一個抽象類,向外部提供了兩個公共方法get和recycle分別用於從對象池中獲取對象和回收對象;另外還提供了一個protected的抽象方法newObject,newObject用於在內存池中沒有可用對象的時候創建新的對象,由用戶自己實現,Recycler以泛型參數的形式讓用戶傳入具體要池化的對象類型。
/** * Light-weight object pool based on a thread-local stack. * * @param <T> the type of the pooled object */ public abstract class Recycler<T>
Recycler內部主要包含三個核心組件,各個組件負責對象池實現的具體部分,Recycler向外部提供統一的對象創建和回收接口:
-
Handle
-
WeakOrderQueue
-
Stack
各組件的功能如下
Handle
Recycler在內部類中給出了Handle的一個默認實現:DefaultHandle,Handle主要提供一個recycle接口,用於提供對象回收的具體實現,每個Handle關聯一個value字段,用於存放具體的池化對象,記住,在對象池中,所有的池化對象都被這個Handle包裝,Handle是對象池管理的基本單位。另外Handle指向這對應的Stack,對象存儲也就是Handle存儲的具體地方由Stack維護和管理。
Stack
Stack具體維護着對象池數據,向Recycler提供push和pop兩個主要訪問接口,pop用於從內部彈出一個可被重復使用的對象,push用於回收以后可以重復使用的對象。
WeakOrderQueue
WeakOrderQueue的功能可以由兩個接口體現,add和transfer。add用於將handler(對象池管理的基本單位)放入隊列,transfer用於向stack輸入可以被重復使用的對象。我們可以把WeakOrderQueue看做一個對象倉庫,stack內只維護一個Handle數組用於直接向Recycler提供服務,當從這個數組中拿不到對象的時候則會尋找對應WeakOrderQueue並調用其transfer方法向stack供給對象。
Recycler實現原理
我先給出一張總的示意圖,下面如果有看不懂的地方可以結合這張圖來理解:
上圖代表着Recycler的工作示意圖。Recycler#get是向外部提供的從對象池獲取對象的接口:
public final T get() { Stack<T> stack = threadLocal.get(); DefaultHandle handle = stack.pop(); if (handle == null) { handle = stack.newHandle(); handle.value = newObject(handle); } return (T) handle.value; }
Recycler首先從當前線程綁定的值中獲取stack,我們可以得知Netty中其實是每個線程關聯着一個對象池,直接關聯對象為Stack,先看看池中是否有可用對象,如果有則直接返回,如果沒有則新創建一個Handle,並且調用newObject來新創建一個對象並且放入Handler的value中,newObject由用戶自己實現。
當Recycler使用Stack的pop接口的時候,我們看看:
DefaultHandle pop() { int size = this.size; if (size == 0) { if (!scavenge()) { return null; } size = this.size; } size --; DefaultHandle ret = elements[size]; if (ret.lastRecycledId != ret.recycleId) { throw new IllegalStateException("recycled multiple times"); } ret.recycleId = 0; ret.lastRecycledId = 0; this.size = size; return ret; }
首先看看Stack的elements數組是否有對象可用,如果有則將size大小減1,返回對象。如果elements數組中已經沒有對象可用,則需要從倉庫中查找是夠有可以用的對象,也就是scavenge的實現,scavenge具體調用的是scavengeSome。Stack的倉庫是由WeakOrderQueue連接起來的鏈表實現的,Stack維護着鏈表的頭部指針。而每個WeakOrderQueue又維護着一個鏈表,節點由Link實現,Link的實現很簡單,主要是繼承AtomicInteger類另外還有一個Handle數組、一個讀指針和一個指向下一個節點的指針,Link巧妙的利用AtomicInteger值來充當數組的寫指針從而避免並發問題。
Recycler對象池的對象存儲分為兩個部分,Stack的Handle數組和Stack指向的WeakOrderQueue鏈表。
private DefaultHandle[] elements; private volatile WeakOrderQueue head; private WeakOrderQueue cursor, prev;
Stack保留着WeakOrderQueue鏈表的頭指針和讀游標。WeakOrderQueue鏈表的每個節點都是一個Link,而每個Link都維護者一個Handle數組。
池中對象的讀取和寫入
從對象池獲取對象主要是從Stack的Handle數組,而Handle數組的后備資源來源於WeakOrderQueue鏈表。而elements數組和WeakOrderQueue鏈表中對象的來源有些區別:
public void recycle() { Thread thread = Thread.currentThread(); if (thread == stack.thread) { stack.push(this); return; } // we don't want to have a ref to the queue as the value in our weak map // so we null it out; to ensure there are no races with restoring it later // we impose a memory ordering here (no-op on x86) Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get(); WeakOrderQueue queue = delayedRecycled.get(stack); if (queue == null) { delayedRecycled.put(stack, queue = new WeakOrderQueue(stack, thread)); } queue.add(this); }
從Handle的recycle實現看出:如果由擁有Stack的線程回收對象,則直接調用Stack的push方法將該對象直接放入Stack的數組中;如果由其他線程回收,則對象被放入線程關聯的<Stack,WeakOrderQueue>的隊列中,這個隊列其實在這里被放入了stack關聯的WeakOrderQueue鏈表的表頭:
WeakOrderQueue(Stack<?> stack, Thread thread) { head = tail = new Link(); owner = new WeakReference<Thread>(thread); synchronized (stack) { next = stack.head; stack.head = this; } }
每一個沒有擁有stack的線程回收對象的時候都會重新創建一個WeakOrderQueue節點放入stask關聯的WeakOrderQueue鏈表的表頭,這樣做最終實現了多線程回收對象統統放入stack關聯的WeakOrderQueue鏈表中而擁有stack的線程都能夠讀取其他線程供給的對象。
簡單的測試數據說話
下面我們來看下基於輕量級內存池和原始使用方式帶來的性能數據對比,這里拿Netty提供的一個簡單的可以回收的RecyclableArrayList來和傳統的ArrayList來做比較,由於RecyclableArrayList和傳統的ArrayList優勢主要在於當頻繁重復創建ArrayList對象的時候RecyclableArrayList不會真的新創建,而是會從池中獲取對象來使用,而ArrayList的每次new操作都會在JVM的對內存中真槍實彈的創建一個對象,因此我們可以想象對於ArrayList的使用,青年代的內存回收相對會比較頻繁,為了簡單期間,我們這個例子不涉及直接內存技術,因此我們關心的地方主要是GC頻率回收的改善,看看我的兩段測試代碼:
代碼1:
public static void main(String ...s) { int i=0, times = 1000000; byte[] data = new byte[1024]; while (i++ < times) { RecyclableArrayList list = RecyclableArrayList.newInstance(); int count = 100; for (int j=0;j<count;j++){ list.add(data); } list.recycle(); System.out.println("count:[" + count + "]"); sleep(1); } }
代碼2:
public static void main(String ...s) { int i=0, times = 1000000; byte[] data = new byte[1024]; while (i++ < times) { ArrayList list = new ArrayList(); int count = 100; for (int j=0;j<count;j++){ list.add(data); } System.out.println("count:[" + count + "]"); sleep(1); } }
上面代碼邏輯相同,分別循環100w次,每次循環創建一個ArrayList對象,放入100個指向1kb大小的字節數組的引用,消耗內存的地方主要是ArrayList對象的創建,因為ArrayList的內部是對象數組實現的,因此內存消耗比較少,我們只能通過快速的循環創建來達到內存漸變的效果。
上面左圖是使用傳統的ArrayList測試數據,右圖是使用RecyclableArrayList的測試數據,對於不可循環使用的ArrayList,GC頻率相比使用RecyclableArrayList的GC頻率高很多,上面的工具也給出了左圖16次GC花費的時間為77.624ms而右圖的3次GC花費的時間為26.740ms。
Recycler對象池總結
在Netty中,所有的IO操作基本上都要涉及緩沖區的使用,無論是上文說的HeapBuffer還是DirectBuffer,如果對於這些緩沖區不能夠重復利用,后果是可想而知的。對於堆內存則會引發相對頻繁的GC,而對於直接內存則會引發頻繁的緩沖區創建與回收,這些操作對於兩種緩沖區分別帶來嚴重的性能損耗,Netty基於ThreadLocal實現的輕量級對象池實現在一定程度上減少了由於GC和分配回收帶來的性能損耗,使得Netty線程運行的更快,總體性能更優。
總體上基於內存池技術的緩沖區實現,優點可以總結如下:
-
對於PooledHeapBuffer的使用,Netty可以重復使用堆內存區域,降低的內存申請的頻率同時也降低了JVM GC的頻率。
-
對於PooledDirectBuffer而言,Netty可以重復使用直接內存區域分配的緩沖區,這使得對於直接內存的使用在原有相比HeapBuffer的優點之外又彌補了自身分配與回收代價相對比較大的缺點。