一、堆外內存定義
內存對象分配在Java虛擬機的堆之外的內存,這些內存直接受操做系統管理(而不是虛擬機),這樣作的結果就是可以在必定程度上減小垃圾回收對應用程序形成的影響。使用未公開的Unsafe和NIO包下ByteBuffer來建立堆外內存。框架
二、爲何使用堆外內存
一、減小了垃圾回收this
使用堆外內存的話,堆外內存是直接受操做系統管理( 而不是虛擬機 )。這樣作的結果就是能保持一個較小的堆內內存,以減小垃圾收集對應用的影響。spa
二、提高複製速度(io效率)操作系統
堆內內存由JVM管理,屬於「用戶態」;而堆外內存由OS管理,屬於「內核態」。若是從堆內向磁盤寫數據時,數據會被先複製到堆外內存,即內核緩衝區,而後再由OS寫入磁盤,使用堆外內存避免了這個操做。線程
三、堆外內存申請
JDK的ByteBuffer類提供了一個接口allocateDirect(int capacity)進行堆外內存的申請,底層經過unsafe.allocateMemory(size)實現。Netty、Mina等框架提供的接口也是基於ByteBuffer封裝的。code
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
能夠對申請的堆外內存進行釋放。orm
在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); } }
四、堆外內存釋放
當初始化一塊堆外內存時,對象的引用關係以下:blog

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

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