目錄
- 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;
}
總結
本文其實還有很多不清楚的地方,對於虛引用以及引用隊列的操作還不是很清楚去,對於虛引用和堆外內存的回收的關系源碼其實也沒看到,
需要再看吧,寫這篇的目的其實最開始就是想研究看看直接緩沖區內存的分配,沒想到依然糊塗,后面填坑。路過的大佬也就指導下虛引用這部分相關的東西,謝謝。
