1碉京、什么是堆外內(nèi)存
堆外內(nèi)存是相對于堆內(nèi)內(nèi)存的一個概念僵芹。堆內(nèi)內(nèi)存是由JVM所管控的Java進(jìn)程內(nèi)存瀑志,我們平時在Java中創(chuàng)建的對象都處于堆內(nèi)內(nèi)存中,并且它們遵循JVM的內(nèi)存管理機(jī)制脖阵,JVM會采用垃圾回收機(jī)制統(tǒng)一管理它們的內(nèi)存皂股。
堆外內(nèi)存使用Native函數(shù)庫(通過Unsafe類的allocateMemory()方法申請分配內(nèi)存,底層會調(diào)用操作系統(tǒng)的的malloc函數(shù))直接分配(native堆)命黔,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作呜呐。
2、堆外內(nèi)存解決了什么問題
解決HeapByteBuffer存在的問題:
如果os和jvm都是用jvm里邊的數(shù)據(jù)區(qū)域悍募, 但是jvm會對這塊內(nèi)存區(qū)域進(jìn)行GC回收蘑辑,可能會對這塊內(nèi)存的數(shù)據(jù)進(jìn)行更改,根據(jù)我們的假設(shè)坠宴,由于這塊區(qū)域os也在使用以躯,jvm對這塊共享數(shù)據(jù)發(fā)生了變更,os那邊就會出現(xiàn)數(shù)據(jù)錯亂的情況。那么如果不讓jvm對這塊共享區(qū)域進(jìn)行GC是不是可以避免這個問題呢忧设?答案是不行的,也會存在問題颠通,如果jvm不對其進(jìn)行GC回收址晕,jvm這邊可能會出現(xiàn)OOM的內(nèi)存溢出。因此只能拷貝jvm的那一份到os的內(nèi)存空間顿锰,即使jvm那邊的數(shù)據(jù)區(qū)域被改變谨垃,但是os里邊的不會受到影響,等os使用io結(jié)束后會對這塊區(qū)域進(jìn)行回收硼控,因?yàn)檫@是os的管理范圍之內(nèi)刘陶。這樣就造成性能降低。
因此牢撼,在JDK1.4中新加入了NIO匙隔,引入了一種基于通道(Channel)和緩存區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存(native堆)熏版,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作纷责。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)撼短。
3再膳、堆外內(nèi)存的實(shí)現(xiàn)
DirectByteBuffer 對象引用位于 Java 內(nèi)存模型的堆里面,JVM 可以對 DirectByteBuffer 的對象進(jìn)行內(nèi)存分配和回收管理曲横,一般使用 DirectByteBuffer 的靜態(tài)方法 allocateDirect() 創(chuàng)建 DirectByteBuffer 實(shí)例并分配內(nèi)存喂柒。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer 內(nèi)部的字節(jié)緩沖區(qū)位在于堆外的(用戶態(tài))直接內(nèi)存,它是通過 Unsafe 的本地方法 allocateMemory() 進(jìn)行內(nèi)存分配禾嫉,底層調(diào)用的是操作系統(tǒng)的 malloc() 函數(shù)灾杰。
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
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)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
除此之外,初始化 DirectByteBuffer 時還會創(chuàng)建一個 Deallocator 線程夭织,并通過 Cleaner 的 freeMemory() 方法來對直接內(nèi)存進(jìn)行回收操作吭露,freeMemory() 底層調(diào)用的是操作系統(tǒng)的 free() 函數(shù)。
private static class Deallocator implements Runnable {
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
由于使用 DirectByteBuffer 分配的是系統(tǒng)本地的內(nèi)存尊惰,不在 JVM 的管控范圍之內(nèi)讲竿,因此直接內(nèi)存的回收和堆內(nèi)存的回收不同,直接內(nèi)存如果使用不當(dāng)弄屡,很容易造成 OutOfMemoryError题禀。
DirectByteBuffer 和零拷貝有什么關(guān)系?
DirectByteBuffer 是 MappedByteBuffer 的具體實(shí)現(xiàn)類膀捷。實(shí)際上迈嘹,Util.newMappedByteBuffer() 方法通過反射機(jī)制獲取 DirectByteBuffer 的構(gòu)造器,然后創(chuàng)建一個 DirectByteBuffer 的實(shí)例,對應(yīng)的是一個單獨(dú)用于內(nèi)存映射的構(gòu)造方法:
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper) {
super(-1, 0, cap, cap, fd);
address = addr;
cleaner = Cleaner.create(this, unmapper);
att = null;
}
在 MappedByteBuffer 進(jìn)行內(nèi)存映射時秀仲,它的 map() 方法會通過 Util.newMappedByteBuffer() 來創(chuàng)建一個緩沖區(qū)實(shí)例融痛,初始化的代碼如下:
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd,
Runnable unmapper) {
MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
initDBBConstructor();
try {
dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size), new Long(addr), fd, unmapper });
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new InternalError(e);
}
return dbb;
}
private static void initDBBRConstructor() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
Class<?> cl = Class.forName("java.nio.DirectByteBufferR");
Constructor<?> ctor = cl.getDeclaredConstructor(
new Class<?>[] { int.class, long.class, FileDescriptor.class,
Runnable.class });
ctor.setAccessible(true);
directByteBufferRConstructor = ctor;
} catch (ClassNotFoundException | NoSuchMethodException |
IllegalArgumentException | ClassCastException x) {
throw new InternalError(x);
}
return null;
}});
}
因此,除了允許分配操作系統(tǒng)的直接內(nèi)存以外神僵,DirectByteBuffer 本身也具有文件內(nèi)存映射的功能雁刷。我們需要關(guān)注的是,DirectByteBuffer 在 MappedByteBuffer 的基礎(chǔ)上提供了內(nèi)存映像文件的隨機(jī)讀取 get() 和寫入 write() 的操作保礼。
內(nèi)存映像文件的隨機(jī)讀操作
public byte get() {
return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
內(nèi)存映像文件的隨機(jī)寫操作
public ByteBuffer put(byte x) {
unsafe.putByte(ix(nextPutIndex()), ((x)));
return this;
}
public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
}
內(nèi)存映像文件的隨機(jī)讀寫都是借助 ix() 方法實(shí)現(xiàn)定位的沛励, ix() 方法通過內(nèi)存映射空間的內(nèi)存首地址(address)和給定偏移量 i 計(jì)算出指針地址,然后由 unsafe 類的 get() 和 put() 方法和對指針指向的數(shù)據(jù)進(jìn)行讀取或?qū)懭搿?/p>
private long ix(int i) {
return address + ((long)i << 0);
}