Nio buffer詳解

轉自:http://zachary-guo.iteye.com/blog/1457542
Buffer 類是 java.nio 的構造基礎。一個 Buffer 對象是固定數量的數據的容器,其作用是一個存儲器,或者分段運輸區,在這裏,數據可被存儲並在之後用於檢索。緩衝區可以被寫滿或釋放。對於每個非布爾原始數據類型都有一個緩衝區類,即 Buffer 的子類有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是沒有 BooleanBuffer 之說的。儘管緩衝區作用於它們存儲的原始數據類型,但緩衝區十分傾向於處理字節。非字節緩衝區可以在後臺執行從字節或到字節的轉換,這取決於緩衝區是如何創建的。
◇ 緩衝區的四個屬性
所有的緩衝區都具有四個屬性來提供關於其所包含的數據元素的信息,這四個屬性儘管簡單,但其至關重要,需熟記於心:

容量(Capacity):緩衝區能夠容納的數據元素的最大數量。這一容量在緩衝區創建時被設定,並且永遠不能被改變。
上界(Limit):緩衝區的第一個不能被讀或寫的元素。緩衝創建時,limit 的值等於 capacity 的值。假設 capacity = 1024,我們在程序中設置了 limit = 512,說明,Buffer 的容量爲 1024,但是從 512 之後既不能讀也不能寫,因此可以理解成,Buffer 的實際可用大小爲 512。
位置(Position):下一個要被讀或寫的元素的索引。位置會自動由相應的 get() 和 put() 函數更新。 這裏需要注意的是positon的位置是從0開始的。
標記(Mark):一個備忘位置。標記在設定前是未定義的(undefined)。使用場景是,假設緩衝區中有 10 個元素,position 目前的位置爲 2(也就是如果get的話是第三個元素),現在只想發送 6 - 10 之間的緩衝數據,此時我們可以 buffer.mark(buffer.position()),即把當前的 position 記入 mark 中,然後 buffer.postion(6),此時發送給 channel 的數據就是 6 - 10 的數據。發送完後,我們可以調用 buffer.reset() 使得 position = mark,因此這裏的 mark 只是用於臨時記錄一下位置用的。

請切記,在使用 Buffer 時,我們實際操作的就是這四個屬性的值。我們發現,Buffer 類並沒有包括 get() 或 put() 函數。但是,每一個Buffer 的子類都有這兩個函數,但它們所採用的參數類型,以及它們返回的數據類型,對每個子類來說都是唯一的,所以它們不能在頂層 Buffer 類中被抽象地聲明。它們的定義必須被特定類型的子類所遵從。若不加特殊說明,我們在下面討論的一些內容,都是以 ByteBuffer 爲例,當然,它當然有 get() 和 put() 方法了。
◇ 相對存取和絕對存取

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
// This is a partial API listing
public abstract byte get( );  
public abstract byte get (int index);  
public abstract ByteBuffer put (byte b);  
public abstract ByteBuffer put (int index, byte b); 
} 

    來看看上面的代碼,有不帶索引參數的方法和帶索引參數的方法。不帶索引的 get 和 put,這些調用執行完後,position 的值會自動前進。當然,對於 put,如果調用多次導致位置超出上界(注意,是 limit 而不是 capacity),則會拋出 BufferOverflowException 異常;對於 get,如果位置不小於上界(同樣是 limit 而不是 capacity),則會拋出 BufferUnderflowException 異常。這種不帶索引參數的方法,稱爲相對存取,相對存取會自動影響緩衝區的位置屬性。帶索引參數的方法,稱爲絕對存取,絕對存儲不會影響緩衝區的位置屬性,但如果你提供的索引值超出範圍(負數或不小於上界),也將拋出 IndexOutOfBoundsException 異常。

◇ 翻轉
我們把 hello 這個串通過 put 存入一 ByteBuffer 中,如下所示:將 hello 存入 ByteBuffer 中

Java代碼

ByteBuffer buffer = ByteBuffer.allocate(1024); 
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o'); 

    此時,position = 5,limit = capacity = 1024。現在我們要從正確的位置從 buffer 讀數據,我們可以把 position 置爲 0,那麼字符串的結束位置在哪呢?這裏上界該出場了。如果把上界設置成當前 position 的位置,即 5,那麼 limit 就是結束的位置。上界屬性指明瞭緩衝區有效內容的末端。人工實現翻轉:

Java代碼

buffer.limit(buffer.position()).position(0); 

    但這種從填充到釋放狀態的緩衝區翻轉是API設計者預先設計好的,他們爲我們提供了一個非常便利的函數:buffer.flip()。另外,rewind() 函數與 flip() 相似,但不影響上界屬性,它只是將位置值設回 0。在進行buffer讀操作的時候,一般都會使用buffer.flip()函數。 

◇ 釋放(Drain)
這裏的釋放,指的是緩衝區通過 put 填充數據後,然後被讀出的過程。上面講了,要讀數據,首先得翻轉。那麼怎麼讀呢?hasRemaining() 會在釋放緩衝區時告訴你是否已經達到緩衝區的上界:hasRemaining()函數和Remaining()函數有密切的功能,

Java代碼

for (int i = 0; buffer.hasRemaining(); i++) { 
    myByteArray[i] = buffer.get(); 
} 

    很明顯,上面的代碼,每次都要判斷元素是否到達上界。我們可以做:改變後的釋放過程

Java代碼

int count = buffer.hasRemaining(); 
for (int i = 0; i < count; i++) { 
    myByteArray[i] = buffer.get(); 
} 

    第二段代碼看起來很高效,但請注意,緩衝區並不是多線程安全的。如果你想以多線程同時存取特定的緩衝區,你需要在存取緩衝區之前進行同步。因此,使用第二段代碼的前提是,你對緩衝區有專門的控制。

◇ buffer.clear()
clear() 函數將緩衝區重置爲空狀態。它並不改變緩衝區中的任何數據元素,而是僅僅將 limit 設爲容量的值,並把 position 設回 0。

◇ Compact(不知咋翻譯,壓縮?緊湊?)
有時候,我們只想釋放出一部分數據,即只讀取部分數據。當然,你可以把 postion 指向你要讀取的第一個數據的位置,將 limit 設置成最後一個元素的位置 + 1。但是,一旦緩衝區對象完成填充並釋放,它就可以被重新使用了。所以,緩衝區一旦被讀取出來,已經沒有使用價值了。
以 Mellow 爲例,填充後爲 Mellow,但如果我們僅僅想讀取 llow。讀取完後,緩衝區就可以重新使用了。Me 這兩個位置對於我們而言是沒用的。我們可以將 llow 複製至 0 - 3 上,Me 則被沖掉。但是 4 和 5 仍然爲 o 和 w。這個事我們當然可以自行通過 get 和 put 來完成,但 api 給我們提供了一個 compact() 的函數,此函數比我們自己使用 get 和 put 要高效的多。
這裏寫圖片描述
Compact 之前的緩衝區
buffer.compact() 會使緩衝區的狀態圖如下圖所示:
這裏寫圖片描述
Compact 之後的緩衝區
這裏發生了幾件事:

數據元素 2 - 5 被複制到 0 - 3 位置,位置 4 和 5 不受影響,但現在正在或已經超出了當前位置,因此是“死的”。它們可以被之後的 put() 調用重寫。
Position 已經被設爲被複制的數據元素的數目,也就是說,緩衝區現在被定位在緩衝區中最後一個“存活”元素的後一個位置。
上界屬性被設置爲容量的值,因此緩衝區可以被再次填滿。
調用 compact() 的作用是丟棄已經釋放的數據,保留未釋放的數據,並使緩衝區對重新填充容量準備就緒。該例子中,你當然可以將 Me 之前已經讀過,即已經被釋放過。

◇ 緩衝區的比較
有時候比較兩個緩衝區所包含的數據是很有必要的。所有的緩衝區都提供了一個常規的 equals() 函數用以測試兩個緩衝區的是否相等,以及一個 compareTo() 函數用以比較緩衝區。
兩個緩衝區被認爲相等的充要條件是:

兩個對象類型相同。包含不同數據類型的 buffer 永遠不會相等,而且buffer 絕不會等於非 buffer對象。
兩個對象都剩餘同樣數量(limit - position)的元素。Buffer 的容量不需要相同,而且緩衝區中剩餘數據的索引也不必相同。
在每個緩衝區中應被 get() 函數返回的剩餘數據元素序列([position, limit - 1] 位置對應的元素序列)必須一致。

這裏寫圖片描述

兩個被認爲是相等的緩衝區
這裏寫圖片描述
兩個被認爲是不相等的緩衝區
緩衝區也支持用 compareTo() 函數以詞典順序進行比較,當然,這是所有的緩衝區實現了 java.lang.Comparable 語義化的接口。這也意味着緩衝區數組可以通過調用 java.util.Arrays.sort() 函數按照它們的內容進行排序。
與 equals() 相似,compareTo() 不允許不同對象間進行比較。但 compareTo()更爲嚴格:如果你傳遞一個類型錯誤的對象,它會拋出 ClassCastException 異常,但 equals() 只會返回 false。
比較是針對每個緩衝區你剩餘數據(從 position 到 limit)進行的,與它們在 equals() 中的方式相同,直到不相等的元素被發現或者到達緩衝區的上界。如果一個緩衝區在不相等元素髮現前已經被耗盡,較短的緩衝區被認爲是小於較長的緩衝區。這裏有個順序問題:下面小於零的結果(表達式的值爲 true)的含義是 buffer2 < buffer1。切記,這代表的並不是 buffer1 < buffer2。

Java代碼

if (buffer1.compareTo(buffer2) < 0) { 
// do sth, it means buffer2 < buffer1,not buffer1 < buffer2
    doSth(); 
} 

◇ 批量移動
緩衝區的設計目的就是爲了能夠高效傳輸數據,一次移動一個數據元素並不高效。如你在下面的程序清單中所看到的那樣,buffer API 提供了向緩衝區你外批量移動數據元素的函數:

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
public ByteBuffer get(byte[] dst); 
public ByteBuffer get(byte[] dst, int offset, int length); 
public final ByteBuffer put(byte[] src);  
public ByteBuffer put(byte[] src, int offset, int length); 
} 

    如你在上面的程序清單中所看到的那樣,buffer API 提供了向緩衝區內外批量移動數據元素的函數。以 get 爲例,它將緩衝區中的內容複製到指定的數組中,當然是從 position 開始咯。第二種形式使用 offset 和 length 參數來指定複製到目標數組的子區間。這些批量移動的合成效果與前文所討論的循環是相同的,但是這些方法可能高效得多,因爲這種緩衝區實現能夠利用本地代碼或其他的優化來移動數據。
    批量移動總是具有指定的長度。也就是說,你總是要求移動固定數量的數據元素。因此,get(dist) 和 get(dist, 0, dist.length) 是等價的。
    對於以下幾種情況的數據複製會發生異常:

如果你所要求的數量的數據不能被傳送,那麼不會有數據被傳遞,緩衝區的狀態保持不變,同時拋出BufferUnderflowException異常。
如果緩衝區中的數據不夠完全填滿數組,你會得到一個異常。這意味着如果你想將一個小型緩衝區傳入一個大型數組,你需要明確地指定緩衝區中剩餘的數據長度。

       如果緩衝區存有比數組能容納的數量更多的數據,你可以重複利用如下代碼進行讀取:

Java代碼

byte[] smallArray = new Byte[10]; 
while (buffer.hasRemaining()) { 
int length = Math.min(buffer.remaining(), smallArray.length); 
    buffer.get(smallArray, 0, length); 
// 每取出一部分數據後,即調用 processData 方法,length 表示實際上取到了多少字節的數據
    processData(smallArray, length); 
} 

    put() 的批量版本工作方式相似,只不過它是將數組裏的元素寫入 buffer 中而已,這裏不再贅述。

◇ 創建緩衝區
Buffer 的七種子類,沒有一種能夠直接實例化,它們都是抽象類,但是都包含靜態工廠方法來創建相應類的新實例。這部分討論中,將以 CharBuffer 類爲例,對於其它六種主要的緩衝區類也是適用的。下面是創建一個緩衝區的關鍵函數,對所有的緩衝區類通用(要按照需要替換類名):

Java代碼

public abstract class CharBuffer extends Buffer implements CharSequence, Comparable { 
// This is a partial API listing
public static CharBuffer allocate (int capacity); 
public static CharBuffer wrap (char [] array); 
public static CharBuffer wrap (char [] array, int offset, int length); 
public final boolean hasArray(); 
public final char [] array(); 
public final int arrayOffset(); 
} 

    新的緩衝區是由分配(allocate)或包裝(wrap)操作創建的。分配(allocate)操作創建一個緩衝區對象並分配一個私有的空間來儲存容量大小的數據元素。包裝(wrap)操作創建一個緩衝區對象但是不分配任何空間來儲存數據元素。它使用你所提供的數組作爲存儲空間來儲存緩衝區中的數據元素。demos:

Java代碼

// 這段代碼隱含地從堆空間中分配了一個 char 型數組作爲備份存儲器來儲存 100 個 char 變量。
CharBuffer charBuffer = CharBuffer.allocate (100); 
/**
* 這段代碼構造了一個新的緩衝區對象,但數據元素會存在於數組中。這意味着通過調用 put() 函數造成的對緩
* 衝區的改動會直接影響這個數組,而且對這個數組的任何改動也會對這個緩衝區對象可見。
*/
char [] myArray = new char [100];  
CharBuffer charbuffer = CharBuffer.wrap (myArray); 
/**
* 帶有 offset 和 length 作爲參數的 wrap() 函數版本則會構造一個按照你提供的 offset 和 length 參
* 數值初始化 position 和 limit 的緩衝區。
*
* 這個函數並不像你可能認爲的那樣,創建了一個只佔用了一個數組子集的緩衝區。這個緩衝區可以存取這個數組
* 的全部範圍;offset 和 length 參數只是設置了初始的狀態。調用 clear() 函數,然後對其進行填充,
* 直到超過 limit,這將會重寫數組中的所有元素。
*
* slice() 函數可以提供一個只佔用備份數組一部分的緩衝區。
*
* 下面的代碼創建了一個 position 值爲 12,limit 值爲 54,容量爲 myArray.length 的緩衝區。
*/
CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42); 

    通過 allocate() 或者 wrap() 函數創建的緩衝區通常都是間接的。間接的緩衝區使用備份數組,你可以通過上面列出的 api 函數獲得對這些數組的存取權。
    boolean 型函數 hasArray() 告訴你這個緩衝區是否有一個可存取的備份數組。如果這個函數的返回 true,array() 函數會返回這個緩衝區對象所使用的數組存儲空間的引用。如果 hasArray() 函數返回 false,不要調用 array() 函數或者 arrayOffset() 函數。如果你這樣做了你會得到一個 UnsupportedOperationException 異常。
    如果一個緩衝區是隻讀的,它的備份數組將會是超出 limit 的,即使一個數組對象被提供給 wrap() 函數。調用 array() 函數或 arrayOffset() 會拋出一個 ReadOnlyBufferException 異常以阻止你得到存取權來修改只讀緩衝區的內容。如果你通過其它的方式獲得了對備份數組的存取權限,對這個數組的修改也會直接影響到這個只讀緩衝區。
    arrayOffset(),返回緩衝區數據在數組中存儲的開始位置的偏移量(從數組頭 0 開始計算)。如果你使用了帶有三個參數的版本的 wrap() 函數來創建一個緩衝區,對於這個緩衝區,arrayOffset() 會一直返回 0。不理解嗎?offset 和 length 只是指示了當前的 position 和 limit,是一個瞬間值,可以通過 clear() 來從 0 重新存數據,所以 arrayOffset() 返回的是 0。當然,如果你切分(slice() 函數)了由一個數組提供存儲的緩衝區,得到的緩衝區可能會有一個非 0 的數組偏移量。

◇ 複製緩衝區
緩衝區不限於管理數組中的外部數據,它們也能管理其他緩衝區中的外部數據。當一個管理其他緩衝器所包含的數據元素的緩衝器被創建時,這個緩衝器被稱爲視圖緩衝器。
視圖存儲器總是通過調用已存在的存儲器實例中的函數來創建。使用已存在的存儲器實例中的工廠方法意味着視圖對象爲原始存儲器的你部實現細節私有。數據元素可以直接存取,無論它們是存儲在數組中還是以一些其他的方式,而不需經過原始緩衝區對象的 get()/put() API。如果原始緩衝區是直接緩衝區,該緩衝區(視圖緩衝區)的視圖會具有同樣的效率優勢。
繼續以 CharBuffer 爲例,但同樣的操作可被用於任何基本的緩衝區類型。用於複製緩衝區的 api:

Java代碼

public abstract class CharBuffer extends Buffer implements CharSequence, Comparable { 
// This is a partial API listing
public abstract CharBuffer duplicate(); 
public abstract CharBuffer asReadOnlyBuffer(); 
public abstract CharBuffer slice(); 
} 

● duplidate()
複製一個緩衝區會創建一個新的 Buffer 對象,但並不複製數據。原始緩衝區和副本都會操作同樣的數據元素。
duplicate() 函數創建了一個與原始緩衝區相似的新緩衝區。兩個緩衝區共享數據元素,擁有同樣的容量,但每個緩衝區擁有各自的 position、limit 和 mark 屬性。對一個緩衝區你的數據元素所做的改變會反映在另外一個緩衝區上。這一副本緩衝區具有與原始緩衝區同樣的數據視圖。如果原始的緩衝區爲只讀,或者爲直接緩衝區,新的緩衝區將繼承這些屬性。duplicate() 複製緩衝區:

Java代碼

CharBuffer buffer = CharBuffer.allocate(8); 
buffer.position(3).limit(6).mark().position (5); 
CharBuffer dupeBuffer = buffer.duplicate(); 
buffer.clear(); 

這裏寫圖片描述

複製一個緩衝區
● asReadOnlyBuffer()
asReadOnlyBuffer() 函數來生成一個只讀的緩衝區視圖。這與duplicate() 相同,除了這個新的緩衝區不允許使用 put(),並且其 isReadOnly() 函數將會返回 true。
如果一個只讀的緩衝區與一個可寫的緩衝區共享數據,或者有包裝好的備份數組,那麼對這個可寫的緩衝區或直接對這個數組的改變將反映在所有關聯的緩衝區上,包括只讀緩衝區。
● slice()
分割緩衝區與複製相似,但 slice() 創建一個從原始緩衝區的當前 position 開始的新緩衝區,並且其容量是原始緩衝區的剩餘元素數量(limit - position)。這個新緩衝區與原始緩衝區共享一段數據元素子序列。分割出來的緩衝區也會繼承只讀和直接屬性。slice() 分割緩衝區:

Java代碼

CharBuffer buffer = CharBuffer.allocate(8); 
buffer.position(3).limit(5); 
CharBuffer sliceBuffer = buffer.slice(); 

這裏寫圖片描述

創建分割緩衝區
◇ 字節緩衝區(ByteBuffer)
ByteBuffer 只是 Buffer 的一個子類,但字節緩衝區有字節的獨特之處。字節緩衝區跟其他緩衝區類型最明顯的不同在於,它可以成爲通道所執行的 I/O 的源頭或目標,後面你會發現通道只接收 ByteBuffer 作爲參數。
字節是操作系統及其 I/O 設備使用的基本數據類型。當在 JVM 和操作系統間傳遞數據時,將其他的數據類型拆分成構成它們的字節是十分必要的,系統層次的 I/O 面向字節的性質可以在整個緩衝區的設計以及它們互相配合的服務中感受到。同時,操作系統是在內存區域中進行 I/O 操作。這些內存區域,就操作系統方面而言,是相連的字節序列。於是,毫無疑問,只有字節緩衝區有資格參與 I/O 操作。
非字節類型的基本類型,除了布爾型都是由組合在一起的幾個字節組成的。那麼必然要引出另外一個問題:字節順序。
多字節數值被存儲在內存中的方式一般被稱爲 endian-ness(字節順序)。如果數字數值的最高字節 - big end(大端),位於低位地址(即 big end 先寫入內存,先寫入的內存的地址是低位的,後寫入內存的地址是高位的),那麼系統就是大端字節順序。如果最低字節最先保存在內存中,那麼系統就是小端字節順序。在 java.nio 中,字節順序由 ByteOrder 類封裝:

Java代碼

package java.nio; 
public final class ByteOrder { 
public static final ByteOrder BIG_ENDIAN; 
public static final ByteOrder LITTLE_ENDIAN; 
public static ByteOrder nativeOrder(); 
public String toString(); 
} 

    ByteOrder 類定義了決定從緩衝區中存儲或檢索多字節數值時使用哪一字節順序的常量。如果你需要知道 JVM 運行的硬件平臺的固有字節順序,請調用靜態類函數 nativeOrder()。

每個緩衝區類都具有一個能夠通過調用 order() 查詢的當前字節順序:

Java代碼

public abstract class CharBuffer extends Buffer implements Comparable, CharSequence { 
// This is a partial API listing
public final ByteOrder order(); 
} 

    這個函數從 ByteOrder 返回兩個常量之一。對於除了 ByteBuffer 之外的其他緩衝區類,字節順序是一個只讀屬性,並且可能根據緩衝區的建立方式而採用不同的值。除了 ByteBuffer,其他通過 allocate() 或 wrap() 一個數組所創建的緩衝區將從 order() 返回與 ByteOrder.nativeOrder() 相同的數值。這是因爲包含在緩衝區中的元素在 JVM 中將會被作爲基本數據直接存取。
    ByteBuffer 類有所不同:默認字節順序總是 ByteBuffer.BIG_ENDIAN,無論系統的固有字節順序是什麼。Java 的默認字節順序是大端字節順序,這允許類文件等以及串行化的對象可以在任何 JVM 中工作。如果固有硬件字節順序是小端,這會有性能隱患。在使用固有硬件字節順序時,將 ByteBuffer 的內容當作其他數據類型存取很可能高效得多。
    爲什麼 ByteBuffer 類需要一個字節順序?字節不就是字節嗎?ByteBuffer 對象像其他基本數據類型一樣,具有大量便利的函數用於獲取和存放緩衝區內容。這些函數對字節進行編碼或解碼的方式取決於 ByteBuffer 當前字節順序的設定。ByteBuffer 的字節順序可以隨時通過調用以 ByteOrder.BIG_ENDIAN 或 ByteOrder.LITTL_ENDIAN 爲參數的 order() 函數來改變:

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
// This is a partial API listing
public final ByteOrder order(); 
public final ByteBuffer order(ByteOrder bo); 
} 

    如果一個緩衝區被創建爲一個 ByteBuffer 對象的視圖,,那麼 order() 返回的數值就是視圖被創建時其創建源頭的 ByteBuffer 的字節順序。視圖的字節順序設定在創建後不能被改變,而且如果原始的字節緩衝區的字節順序在之後被改變,它也不會受到影響。

◇ 直接緩衝區
內核空間(與之相對的是用戶空間,如 JVM)是操作系統所在區域,它能與設備控制器(硬件)通訊,控制着用戶區域進程(如 JVM)的運行狀態。最重要的是,所有的 I/O 都直接(物理內存)或間接(虛擬內存)通過內核空間。
當進程(如 JVM)請求 I/O 操作的時候,它執行一個系統調用將控制權移交給內核。當內核以這種方式被調用,它隨即採取任何必要步驟,找到進程所需數據,並把數據傳送到用戶空間你的指定緩衝區。內核試圖對數據進行高速緩存或預讀取,因此進程所需數據可能已經在內核空間裏了。如果是這樣,該數據只需簡單地拷貝出來即可。如果數據不在內核空間,則進程被掛起,內核着手把數據讀進內存。
這裏寫圖片描述
I/O 緩衝區操作簡圖
從圖中你可能會覺得,把數據從內核空間拷貝到用戶空間似乎有些多餘。爲什麼不直接讓磁盤控制器把數據送到用戶空間的緩衝區呢?首先,硬件通常不能直接訪問用戶空間。其次,像磁盤這樣基於塊存儲的硬件設備操作的是固定大小的數據塊,而用戶進程請求的可能是任意大小的或非對齊的數據塊。在數據往來於用戶空間與存儲設備的過程中,內核負責數據的分解、再組合工作,因此充當着中間人的角色。
因此,操作系統是在內存區域中進行 I/O 操作。這些內存區域,就操作系統方面而言,是相連的字節序列,這也意味着I/O操作的目標內存區域必須是連續的字節序列。在 JVM中,字節數組可能不會在內存中連續存儲(因爲 JAVA 有 GC 機制),或者無用存儲單元(會被垃圾回收)收集可能隨時對其進行移動。
出於這個原因,引入了直接緩衝區的概念。直接字節緩衝區通常是 I/O 操作最好的選擇。非直接字節緩衝區(即通過 allocate() 或 wrap() 創建的緩衝區)可以被傳遞給通道,但是這樣可能導致性能損耗。通常非直接緩衝不可能成爲一個本地 I/O 操作的目標。
如果你向一個通道中傳遞一個非直接 ByteBuffer 對象用於寫入,通道可能會在每次調用中隱含地進行下面的操作:

創建一個臨時的直接 ByteBuffer 對象。
將非直接緩衝區的內容複製到臨時直接緩衝區中。
使用臨時直接緩衝區執行低層 I/O 操作。
臨時直接緩衝區對象離開作用域,並最終成爲被回收的無用數據。

       這可能導致緩衝區在每個 I/O 上覆制併產生大量對象,而這種事都是我們極力避免的。如果你僅僅爲一次使用而創建了一個緩衝區,區別並不是很明顯。另一方面,如果你將在一段高性能腳本中重複使用緩衝區,分配直接緩衝區並重新使用它們會使你遊刃有餘。
    直接緩衝區可能比創建非直接緩衝區要花費更高的成本,它使用的內存是通過調用本地操作系統方面的代碼分配的,繞過了標準 JVM 堆棧,不受垃圾回收支配,因爲它們位於標準 JVM 堆棧之外。
    直接 ByteBuffer 是通過調用具有所需容量的 ByteBuffer.allocateDirect() 函數產生的。注意,wrap() 函數所創建的被包裝的緩衝區總是非直接的。與直接緩衝區相關的 api:

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
// This is a partial API listing
public static ByteBuffer allocateDirect (int capacity); 
public abstract boolean isDirect(); 
} 

    所有的緩衝區都提供了一個叫做 isDirect() 的 boolean 函數,來測試特定緩衝區是否爲直接緩衝區。但是,ByteBuffer 是唯一可以被分配成直接緩衝區的 Buffer。儘管如此,如果基礎緩衝區是一個直接 ByteBuffer,對於非字節視圖緩衝區,isDirect() 可以是 true。

◇ 視圖緩衝區
I/O 基本上可以歸結成組字節數據的四處傳遞,在進行大數據量的 I/O 操作時,很又可能你會使用各種 ByteBuffer 類去讀取文件內容,接收來自網絡連接的數據,等等。ByteBuffer 類提供了豐富的 API 來創建視圖緩衝區。
視圖緩衝區通過已存在的緩衝區對象實例的工廠方法來創建。這種視圖對象維護它自己的屬性,容量,位置,上界和標記,但是和原來的緩衝區共享數據元素。
每一個工廠方法都在原有的 ByteBuffer 對象上創建一個視圖緩衝區。調用其中的任何一個方法都會創建對應的緩衝區類型,這個緩衝區是基礎緩衝區的一個切分,由基礎緩衝區的位置和上界決定。新的緩衝區的容量是字節緩衝區中存在的元素數量除以視圖類型中組成一個數據類型的字節數,在切分中任一個超過上界的元素對於這個視圖緩衝區都是不可見的。視圖緩衝區的第一個元素從創建它的 ByteBuffer 對象的位置開始(positon() 函數的返回值)。來自 ByteBuffer 創建視圖緩衝區的工廠方法:

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
// This is a partial API listing
public abstract CharBuffer asCharBuffer(); 
public abstract CharBuffer asShortBuffer( ); 
public abstract CharBuffer asIntBuffer( ); 
public abstract CharBuffer asLongBuffer( ); 
public abstract CharBuffer asFloatBuffer( ); 
public abstract CharBuffer asDoubleBuffer( ); 
} 

    下面的代碼創建了一個 ByteBuffer 緩衝區的 CharBuffer 視圖。演示 7 個字節的 ByteBuffer 的 CharBuffer 視圖:

Java代碼

/**
* 1 char = 2 byte,因此 7 個字節的 ByteBuffer 最終只會產生 capacity 爲 3 的 CharBuffer。
*
* 無論何時一個視圖緩衝區存取一個 ByteBuffer 的基礎字節,這些字節都會根據這個視圖緩衝區的字節順序設
* 定被包裝成一個數據元素。當一個視圖緩衝區被創建時,視圖創建的同時它也繼承了基礎 ByteBuffer 對象的
* 字節順序設定,這個視圖的字節排序不能再被修改。字節順序設定決定了這些字節對是怎麼樣被組合成字符
* 型變量的,這樣可以理解爲什麼 ByteBuffer 有字節順序的概念了吧。
*/
ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN); 
CharBuffer charBuffer = byteBuffer.asCharBuffer(); 

這裏寫圖片描述
7 個 字節的 ByteBuffer 的 CharBuffer 視圖
◇ 數據元素視圖
ByteBuffer 類爲每一種原始數據類型提供了存取的和轉化的方法:

Java代碼

public abstract class ByteBuffer extends Buffer implements Comparable { 
public abstract short getShort( );  
public abstract short getShort(int index); 
public abstract short getInt( );  
public abstract short getInt(int index); 
    ...... 
public abstract ByteBuffer putShort(short value);  
public abstract ByteBuffer putShort(int index, short value); 
public abstract ByteBuffer putInt(int value);  
public abstract ByteBuffer putInt(int index, int value); 
    ....... 
} 

    這些函數從當前位置開始存取 ByteBuffer 的字節數據,就好像一個數據元素被存儲在那裏一樣。根據這個緩衝區的當前的有效的字節順序,這些字節數據會被排列或打亂成需要的原始數據類型。
    如果 getInt() 函數被調用,從當前的位置開始的四個字節會被包裝成一個 int 類型的變量然後作爲函數的返回值返回。實際的返回值取決於緩衝區的當前的比特排序(byte-order)設置。不同字節順序取得的值是不同的:

Java代碼

// 大端順序
int value = buffer.order(ByteOrder.BIG_ENDIAN).getInt(); 
// 小端順序
int value = buffer.order(ByteOrder.LITTLE_ENDIAN).getInt(); 
// 上述兩種方法取得的 int 是不一樣的,因此在調用此類方法前,請確保字節順序是你所期望的

    如果你試圖獲取的原始類型需要比緩衝區中存在的字節數更多的字節,會拋出 BufferUnderflowException。

原文地址:http://zachary-guo.iteye.com/blog/1457542

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章