1、堆外內存定義
內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。使用未公開的Unsafe和NIO包下ByteBuffer來創建堆外內存。
2、為什么使用堆外內存
1、減少了垃圾回收
使用堆外內存的話,堆外內存是直接受操作系統管理( 而不是虛擬機 )。這樣做的結果就是能保持一個較小的堆內內存,以減少垃圾收集對應用的影響。
2、提升復制速度(io效率)
堆內內存由JVM管理,屬於“用戶態”;而堆外內存由OS管理,屬於“內核態”。如果從堆內向磁盤寫數據時,數據會被先復制到堆外內存,即內核緩沖區,然后再由OS寫入磁盤,使用堆外內存避免了這個操作。
3、堆外內存申請
JDK的ByteBuffer類提供了一個接口allocateDirect(int capacity)進行堆外內存的申請,底層通過unsafe.allocateMemory(size)實現。Netty、Mina等框架提供的接口也是基於ByteBuffer封裝的。
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類保存總分配內存(按頁分配)的大小和實際內存的大小 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; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
注:unsafe.allocateMemory(size)最底層是通過malloc
方法申請的,但是這塊內存需要進行手動釋放,JVM並不會進行回收,幸好Unsafe
提供了另一個接口freeMemory
可以對申請的堆外內存進行釋放。
在Cleaner 內部中通過一個列表,維護了針對每一個 directBuffer 的一個回收堆外內存的線程對象(Runnable),回收操作是發生在 Cleaner 的 clean() 方法中。
private Cleaner(Object var1, Runnable var2) { super(var1, dummyQueue); this.thunk = var2; } public static Cleaner create(Object var0, Runnable var1) { return var1 == null ? null : add(new Cleaner(var0, var1)); } public void clean() { if (remove(this)) { try { this.thunk.run(); //此處會調用Deallocator,見下個類 } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); } } }
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);//unsafe提供的方法釋放內存 address = 0; Bits.unreserveMemory(size, capacity); } }
4、堆外內存釋放
當初始化一塊堆外內存時,對象的引用關系如下:

first
是
Cleaner
類的靜態變量,
Cleaner
對象在初始化時會被添加到
Clener
鏈表中,和
first
形成引用關系,
ReferenceQueue
是用來保存需要回收的
Cleaner
對象。
如果該DirectByteBuffer
對象在一次GC中被回收了

此時,只有Cleaner
對象唯一保存了堆外內存的數據(開始地址、大小和容量),在下一次FGC時,把該Cleaner
對象放入到ReferenceQueue
中,並觸發clean
方法。
Cleaner
對象的clean
方法主要有兩個作用:
1、把自身從Clener
鏈表刪除,從而在下次GC時能夠被回收
2、釋放堆外內存
如果JVM一直沒有執行FGC的話,無效的Cleaner
對象就無法放入到ReferenceQueue中,從而堆外內存也一直得不到釋放,內存豈不是會爆?
其實在初始化DirectByteBuffer
對象時,如果當前堆外內存的條件很苛刻時,會主動調用System.gc()
強制執行FGC。
參考鏈接:https://www.jianshu.com/p/35cf0f348275