更多 Java 虛擬機(jī)方面的文章滩褥,請參見文集《Java 虛擬機(jī)》
為什么需要使用堆外內(nèi)存
- 將長期存活的對象(如 Local Cache )移入堆外內(nèi)存( off-heap衰伯,又名直接內(nèi)存 direct-memory)牧抵,從而減少 CMS 管理的對象數(shù)量, 以降低 Full GC 的次數(shù)和頻率帅刊,達(dá)到提高系統(tǒng)響應(yīng)速度的目的法挨。
- 加快了復(fù)制的速度:堆內(nèi)在 flush 到遠(yuǎn)程時(shí)吱涉,會先復(fù)制到直接內(nèi)存,然后在發(fā)送捺疼;而堆外內(nèi)存相當(dāng)于省略掉了這個(gè)工作疏虫。
堆外內(nèi)存不是 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū) Runtime Data Area 的一部分,這部分內(nèi)存區(qū)域直接被操作系統(tǒng)管理啤呼,JVM 通過 JNI 本地接口操作堆外內(nèi)存卧秘。
堆外內(nèi)存的使用
在 JDK 1.4以前,對這部分內(nèi)存訪問沒有光明正大的做法:只能通過反射拿到 Unsafe 類官扣,然后調(diào)用allocateMemory()/freeMemory()來申請/釋放這塊內(nèi)存翅敌。
1.4 開始新加入了 NIO,它引入了一種基于 Channel 與 Buffer 的 I/O 方式醇锚,可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存哼御,然后通過一個(gè)存儲在 Java 堆里面的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作坯临,ByteBuffer 提供了如下常用方法來跟堆外內(nèi)存打交道:
-
public static ByteBuffer allocateDirect(int capacity)
- 分配堆外內(nèi)存,返回一個(gè)
DirectByteBuffer
堆外內(nèi)存對象return new DirectByteBuffer(capacity);
- 分配堆外內(nèi)存,返回一個(gè)
-
public abstract ByteBuffer put(byte b);
- 向堆外內(nèi)存中存放一個(gè)字節(jié)
-
public abstract byte get();
- 從堆外內(nèi)存中讀取一個(gè)字節(jié)
-
public final ByteBuffer put(byte[] src)
- 向堆外內(nèi)存中存放一個(gè)字節(jié)數(shù)組
-
public ByteBuffer get(byte[] dst)
- 從堆外內(nèi)存中讀取一個(gè)字節(jié)數(shù)組
-
public abstract ByteBuffer putInt(int value);
- 向堆外內(nèi)存中存放一個(gè)
int
- 向堆外內(nèi)存中存放一個(gè)
-
public abstract int getInt();
- 從堆外內(nèi)存中讀取一個(gè)
int
- 從堆外內(nèi)存中讀取一個(gè)
-
public abstract IntBuffer asIntBuffer()
- 轉(zhuǎn)換為一個(gè)
IntBuffer
- 轉(zhuǎn)換為一個(gè)
-
public abstract ByteBuffer putLong(long value);
同上恋昼,以此類推 -
public abstract boolean isDirect();
- 判斷是否為堆外內(nèi)存
ByteBuffer 包含了如下的幾個(gè)屬性:
-
private int mark = -1;
:標(biāo)記位置看靠,記錄當(dāng)前position
的值 -
private int position = 0;
:當(dāng)前位置 -
private int limit;
:限制大小 -
private int capacity;
:空間容量 - 基本關(guān)系
mark <= position <= limit <= capacity
示例如下:
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.allocateDirect(1024);
bb.putChar('A');
bb.putInt(123);
System.out.println("capacity: " + bb.capacity());
System.out.println("limit: " + bb.limit());
System.out.println("position: " + bb.position());
bb.position(0);
System.out.println(bb.getChar());
System.out.println(bb.getInt());
}
輸出:
capacity: 1024
limit: 1024
position: 6
A
123
堆外內(nèi)存的設(shè)置
堆外內(nèi)存的限額默認(rèn)與堆內(nèi)內(nèi)存(由-XMX 設(shè)定)相仿,可用 -XX:MaxDirectMemorySize
重新設(shè)定液肌。
當(dāng)使用達(dá)到了閾值的時(shí)候?qū)⒄{(diào)用 System.gc
來做一次 Full GC挟炬,以此來回收掉沒有被使用的堆外內(nèi)存。
堆外內(nèi)存的分配
在 DirectByteBuffer
中嗦哆,首先向 Bits
類申請額度谤祖,Bits
類有一個(gè)全局的 totalCapacity
變量,記錄著全部 DirectByteBuffer
的總大小老速,每次申請粥喜,都先看看是否超限:
- 如果已經(jīng)超限,會主動(dòng)執(zhí)行
Sytem.gc()
橘券,期待能主動(dòng)回收一點(diǎn)堆外內(nèi)存额湘。然后休眠一百毫秒,看看totalCapacity
降下來沒有旁舰,如果內(nèi)存還是不足锋华,就拋出大家最頭痛的 OOM 異常。 - 如果額度被批準(zhǔn)箭窜,就調(diào)用大名鼎鼎的
sun.misc.Unsafe
去分配內(nèi)存毯焕,返回內(nèi)存基地址,Unsafe
的 C++實(shí)現(xiàn)在此磺樱,標(biāo)準(zhǔn)的malloc
纳猫。然后再調(diào)一次Unsafe
把這段內(nèi)存給清零。
堆外內(nèi)存的回收
堆外內(nèi)存基于 GC 的回收
存在于堆內(nèi)的 DirectByteBuffer
對象很小竹捉,只存著基地址和大小等幾個(gè)屬性续担,和一個(gè) Cleaner
,但它代表著后面所分配的一大段內(nèi)存活孩,是所謂的冰山對象。
通過前面說的 Cleaner
乖仇,堆內(nèi)的 DirectByteBuffer
對象被 GC 時(shí)憾儒,它背后的堆外內(nèi)存也會被回收。
這里可以看到一種尷尬的情況乃沙,因?yàn)?DirectByteBuffer
本身的個(gè)頭很小起趾,只要熬過了 Young GC,即使已經(jīng)失效了也能在老生代里舒服的呆著警儒,不容易把老生代撐爆觸發(fā) Full GC训裆,如果沒有別的大塊頭進(jìn)入老生代觸發(fā)Full GC眶根,就一直在那耗著,占著一大片堆外內(nèi)存不釋放边琉。
這時(shí)属百,就只能靠前面提到的申請額度超限時(shí)觸發(fā)的 System.gc()
來救場了。
堆外內(nèi)存的主動(dòng)回收
對于 Sun 的 JDK 這其實(shí)很簡單变姨,只要從 DirectByteBuffer
里取出那個(gè) sun.misc.Cleaner
族扰,然后調(diào)用它的 clean()
就行。
例如:
((DirectBuffer)bb).cleaner().clean();
引用:
JVM初探——使用堆外內(nèi)存減少Full GC
Netty之Java堆外內(nèi)存掃盲貼
從0到1起步-跟我進(jìn)入堆外內(nèi)存的奇妙世界