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