目錄
- Buffer概述
- Buffer的建立
- Buffer的使用
- 總結
- 參考資料
Buffer概述
注:全文以ByteBuffer
類為例說明
在Java
中提供了7種型別的Buffer
,每一種型別的Buffer
根據分配記憶體的方式不同又可以分為
直接緩衝區和非直接緩衝區。
Buffer
的本質是一個定長陣列,並且在建立的時候需要指明Buffer
的容量(陣列的長度)。
而這個陣列定義在不同的Buffer
當中。例如ByteBuffer
的定義如下:
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
//在這裡定義Buffer對應的陣列,而不是在Heap-X-Buffer中定義
//目的是為了減少訪問這些紙所需的虛方法呼叫,但是對於小的緩衝區,代價比較高
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
//呼叫父類Buffer類的建構函式構造
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// Creates a new buffer with the given mark, position, limit, and capacity
//
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
......
}
儘管陣列在這裡定義,但是這個陣列只對非直接緩衝區有效。
ByteBuffer
類有兩個子類分別是:DirectByteBuffer
(直接緩衝區類)和HeapByteBuffer
(非直接緩衝區)。
但是這兩個類並不能直接被訪問,因為這兩個類是包私有的,而建立這兩種緩衝區的方式就是通過呼叫Buffer
類提供的建立緩衝區的靜態方法:allocate()
和allocateDirect()
。
Buffer的建立
Buffer
要麼是直接的要麼是非直接的,非直接緩衝區的記憶體分配在JVM
記憶體當中,
而直接緩衝區使用實體記憶體對映,直接在實體記憶體中分配緩衝區,既然分配記憶體的地方不一樣,
BUffer
的建立方式也就不一樣。
非直接緩衝區記憶體的分配
建立非直接緩衝區可以通過呼叫allocate()
方法,這樣會將緩衝區建立在JVM
記憶體(堆記憶體)當中。
allocate()
方法是一個靜態方法,因此可以直接使用類來呼叫。
具體的建立過程如下:
/**
* Allocates a new byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. It will have a {@link #array backing array},
* and its {@link #arrayOffset array offset} will be zero.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
//分配一個緩衝區,最後返回的其實是一個HeapByteBuffer的物件
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
//這裡呼叫到HeapByteBuffer類的建構函式,建立非直接緩衝區
//並將需要的Buffer容量傳遞
//從名稱也可以看出,建立的位置在堆記憶體上。
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(capacity, capacity)
用於在堆記憶體上建立一個緩衝區。
該方法優惠調回ByteBuffer
構造方法,HeapByteBuffer
類沒有任何的欄位,他所需的欄位全部定義在父類當中。
原始碼分析如下:
HeapByteBuffer(int cap, int lim) {
// 呼叫父類的構造方法建立非直接緩衝區 // package-private
// 呼叫時根據傳遞的容量建立了一個陣列。
super(-1, 0, lim, cap, new byte[cap], 0);
}
//ByteBuffer類的構造方法,也就是上面程式碼呼叫的super方法
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
//接著呼叫Buffer類的構造方法給用於運算元組的四個屬性賦值
super(mark, pos, lim, cap);
//將陣列賦值給ByteBuffer的hb屬性,
this.hb = hb;
this.offset = offset;
}
//Buffer類的構造方法
Buffer(int mark, int pos, int lim, int cap) { // package-private
//容量引數校驗,原始容量不能小於0
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
//設定容量
this.capacity = cap;
//這裡的lim從上面傳遞過來的時候就是陣列的容量
//limit在寫模式下預設可操作的範圍就是整個陣列
//limit在讀模式下可以操作的範圍是陣列中寫入的元素
//建立的時候就是寫模式,是整個陣列
limit(lim);
//初始的position是0
position(pos);
//設定mark的值,初始情況下是-1,因此有一個引數校驗,
//-1是陣列之外的下標,不可以使用reset方法使得postion到mark的位置。
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
在堆上建立緩衝區還是很簡單的,本質就是建立了一個陣列以及一些用於輔助運算元組的其他屬性。
最後返回的其實是一個HeapByteBuffer
的物件,因此對其的後續操作大多應該是要呼叫到HeapByteBuffer
類中
直接緩衝區的建立
建立直接俄緩衝區可以通過呼叫allocateDirect()
方法建立,原始碼如下:
/**
* Allocates a new direct byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. Whether or not it has a
* {@link #hasArray backing array} is unspecified.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
//建立一個直接緩衝區
public static ByteBuffer allocateDirect(int capacity) {
//同非直接緩衝區,都是建立的子類的物件
//建立一個直接緩衝區物件
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(capacity)
是DirectByteBuffer
的建構函式,具體程式碼如下:
DirectByteBuffer(int cap) { // package-private
//初始化mark,position,limit,capacity
super(-1, 0, cap, cap);
//記憶體是否按頁分配對齊,是的話,則實際申請的記憶體可能會增加達到對齊效果
//預設關閉,可以通過-XX:+PageAlignDirectMemory控制
boolean pa = VM.isDirectMemoryPageAligned();
//獲取每頁記憶體的大小
int ps = Bits.pageSize();
//分配記憶體的大小,如果是按頁對其的方式,需要加一頁記憶體的容量
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//預定記憶體,預定不到則進行回收堆外記憶體,再預定不到則進行Full gc
Bits.reserveMemory(size, cap);
long base = 0;
try {
//分配堆外記憶體
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
/**
*建立堆外記憶體回收Cleanner,Cleanner物件是一個PhantomFerence幽靈引用,
*DirectByteBuffer物件的堆記憶體回收了之後,幽靈引用Cleanner會通知Reference
*物件的守護程式ReferenceHandler對其堆外記憶體進行回收,呼叫Cleanner的
*clean方法,clean方法呼叫的是Deallocator物件的run方法,run方法呼叫的是
*unsafe.freeMemory回收堆外記憶體。
*堆外記憶體minor gc和full gc的時候都不會進行回收,而是ReferenceHandle守護程式呼叫
*cleanner物件的clean方法進行回收。只不過gc 回收了DirectByteBuffer之後,gc會通知Cleanner進行回收
*/
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
由於是在實體記憶體中直接分配一塊記憶體,而java
並不直接操作記憶體需要交給JDK
中native
方法的實現分配
Bits.reserveMemory(size, cap)
預定記憶體原始碼,預定記憶體,說穿了就是檢查堆外記憶體是否足夠分配
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
// 在分配或釋放直接記憶體時應當呼叫這些方法,
// 他們允許用控制程式可以訪問的直接記憶體的數量,所有大小都以位元組為單位
static void reserveMemory(long size, int cap) {
//memoryLimitSet的初始值為false
//獲取允許的最大堆外記憶體賦值給maxMemory,預設為64MB
//可以通過-XX:MaxDirectMemorySize引數控制
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// optimist!
//理想情況,maxMemory足夠分配(有足夠記憶體供預定)
if (tryReserveMemory(size, cap)) {
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
// 這裡會嘗試回收堆外空間,每次回收成功嘗試進行堆外空間的引用
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// trigger VM's Reference processing
// 依然分配失敗嘗試回收堆空間,觸發full gc
//
System.gc();
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
// 接下來會嘗試最多9次的記憶體預定,應該說是9次的回收堆外記憶體失敗的記憶體預定
// 如果堆外記憶體回收成功,則直接嘗試一次記憶體預定,只有回收失敗才會sleep執行緒。
// 每次預定的時間間隔為1ms,2ms,4ms,等2的冪遞增,最多256ms。
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
// 嘗試預定記憶體
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
// 預定記憶體失敗則進行嘗試釋放堆外記憶體,
// 累計最高可以允許釋放堆外記憶體9次,同時sleep執行緒,對應時間以2的指數冪遞增
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
為什麼呼叫System.gc
?引用自JVM原始碼分析之堆外記憶體完全解讀
既然要呼叫System.gc
,那肯定是想通過觸發一次gc
操作來回收堆外部記憶體,不過我想先說的是堆外部記憶體不會對gc
造成什麼影響(這裡的System.gc
除外),但是堆外層記憶體的回收實際上依賴於我們的gc
機制,首先我們要知道在java
尺寸和我們在堆外分配的這塊記憶體分配的只有與之關聯的DirectByteBuffer
物件了,它記錄了這塊記憶體的基地址以及大小,那麼既然和gc
也有關,那就是gc
能通過DirectByteBuffer
物件來間接操作對應的堆外部記憶體了。DirectByteBuffer
物件在建立的時候關聯了一個PhantomReference
,說到PhantomReference
時被回收的,它不能影響gc
方法,但是gc
過程中如果發現某個物件只有只有PhantomReference
引用它之外,並沒有其他的地方引用它了,那將會把這個引用放到java.lang.ref .Reference.pending
物理裡,在gc
完成的時候通知ReferenceHandler
這個守護執行緒去執行一些後置處理,而DirectByteBuffer
關聯的PhantomReference
是PhantomReference
的一個子類,在最終的處理裡會通過Unsafe
的免費介面來釋放DirectByteBuffer
對應的堆外記憶體塊
Buffer的使用
切換讀模式flip()
切換為讀模式的程式碼分廠簡單,就是使limit
指標指向buffer中最後一個插入的元素的位置,即position
,指標的位置。
而position
代表操作的位置,那麼從0開始,所以需要將position
指標歸0.原始碼如下:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
get()讀取
get()
讀取的核心是緩衝區對應的陣列中取出元素放在目標陣列中(get(byte[] dst)
方法是有一個引數的,傳入的就是目標陣列)。
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
//shiyongfor迴圈依次放入目標陣列中
for (int i = offset; i < end; i++)
// get()對於直接緩衝區和非直接緩衝區是不一樣的,所以交由子類實現。
dst[i] = get();
return this;
}
rewind()重複讀
既然要重複讀就需要把position
置0了
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear()清空緩衝區與compact()方法
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
在clear()
方法中,僅僅是將三個指標還原為建立時的狀態供後續寫入,但是之前寫入的資料並沒有被刪除,依然可以使用get(int index)
獲取
但是有一種情況,緩衝區已經滿了還想接著寫入,但是沒有讀取完又不能從頭開始寫入該怎麼辦,答案是compact()
方法
非直接緩衝區:
public ByteBuffer compact() {
//將未讀取的部分拷貝到緩衝區的最前方
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
//設定position位置到緩衝區下一個可以寫入的位置
position(remaining());
//設定limit是最大容量
limit(capacity());
//設定mark=-1
discardMark();
return this;
}
直接緩衝區:
public ByteBuffer compact() {
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
//呼叫native方法拷貝未讀物部分
unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
//設定指標位置
position(rem);
limit(capacity());
discardMark();
return this;
}
mark()標記位置以及reset()還原
mark()
標記一個位置,準確的說是當前的position
位置
public final Buffer mark() {
mark = position;
return this;
}
標記了之後並不影響寫入或者讀取,position
指標從這個位置離開再次想從這個位置讀取或者寫入時,
可以使用reset()
方法
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
總結
本文其實還有很多不清楚的地方,對於虛引用以及引用佇列的操作還不是很清楚去,對於虛引用和堆外記憶體的回收的關係原始碼其實也沒看到,
需要再看吧,寫這篇的目的其實最開始就是想研究看看直接緩衝區記憶體的分配,沒想到依然糊塗,後面填坑。路過的大佬也就指導下虛引用這部分相關的東西,謝謝。